/*
* Copyright (c) 2009, 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.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.apache.log4j.Logger;
import ca.sqlpower.object.ObjectDependentException;
import ca.sqlpower.sqlobject.SQLRelationship.SQLImportedKey;
public class SQLObjectUtils {
private final static Logger logger = Logger.getLogger(SQLObjectUtils.class);
private SQLObjectUtils() {}
/**
* This will check if the two objects given are contained in the same
* root SQLObject by finding their top ancestors and comparing them. o1 and o2
* cannot be null.
*/
public static boolean isInSameSession(SQLObject o1, SQLObject o2) {
SQLObject o1Parent = o1;
while (o1Parent.getParent() != null && o1Parent.getParent() instanceof SQLObject) {
o1Parent = (SQLObject) o1Parent.getParent();
}
SQLObject o2Parent = o2;
while (o2Parent.getParent() != null && o2Parent.getParent() instanceof SQLObject) {
o2Parent = (SQLObject) o2Parent.getParent();
}
logger.debug("Parent of " + o1 + " is " + o1Parent + ", parent of " + o2 + " is " + o2Parent);
return o1Parent == o2Parent;
}
/**
* Creates a dot-separated string of the name of the given SQLObject and the
* names of each of its ancestors, stopping at the first ancestor of the
* given type. The top-level ancestor's name will be the first name to
* appear in the string, and the given object's name will be the last.
*
* @param obj
* The object whose qualified name you wish to obtain
* @param stopAt
* The class of ancestor to stop at. The name of this ancestor
* will not be included in the returned string. If stopAt is
* null, or a class which is not an ancestor of the given
* SQLObject, the returned string will contain all ancestor
* object names up to the root of the SQLObject tree.
*/
public static String toQualifiedName(SQLObject obj, Class<? extends SQLObject> stopAt) {
return toQualifiedName(obj, stopAt, "");
}
/**
* Creates a dot-separated quoted string of the name of the given SQLObject
* and the names of each of its ancestors, stopping at the first ancestor of
* the given type. The top-level ancestor's name will be the first name to
* appear in the string, and the given object's name will be the last.
*
* @param obj
* The object whose qualified name you wish to obtain
* @param stopAt
* The class of ancestor to stop at. The name of this ancestor
* will not be included in the returned string. If stopAt is
* null, or a class which is not an ancestor of the given
* SQLObject, the returned string will contain all ancestor
* object names up to the root of the SQLObject tree.
* @param quote
* The string to quote each SQLObject name with. If no quoting is
* desired set this to the empty string.
*/
public static String toQualifiedName(SQLObject obj, Class<? extends SQLObject> stopAt, String quote) {
List<SQLObject> ancestors = new ArrayList<SQLObject>();
while (obj != null && obj.getClass() != stopAt) {
ancestors.add(obj);
if (!(obj.getParent() instanceof SQLObject)) break;
obj = (SQLObject) obj.getParent();
}
StringBuilder sb = new StringBuilder();
for (int i = ancestors.size() - 1; i >= 0; i--) {
String ancestorName;
ancestorName = quote + ancestors.get(i).getName() + quote;
sb.append(ancestorName);
if (i != 0) {
sb.append(".");
}
}
return sb.toString();
}
/**
* Creates a dot-separated string of the name of the given SQLObject and the
* names of each of its ancestors. The top-level ancestor's name will be the
* first name to appear in the string, and the given object's name will be
* the last. This is the equivalent of calling toQualifiedName(obj, null).
*/
public static String toQualifiedName(SQLObject obj) {
return toQualifiedName(obj, null);
}
/**
* Updates the child list of the given parent object with the new children in the
* given list, in the following way:
*
* <ol>
* <li>For children in both lists, the parent's existing child object is retained,
* but its updateToMatch() method is called to update its properties
* <li>For children in the newChildren list but not in the parent's current
* list of children, the newChildren object is added as a child
* <li>Children of the parent that are not in the newChildren list are removed from
* the parent.
* </ol>
*
* All comparisons are done by child name, and are case sensitive. The caveat is,
* this refresh process will not work properly if the parent contains multiple children
* with exactly the same name.
* <p>
* There is a special case to make this method work with SQLRelationship: if the objects
* in newChildren are SQLRelationships, those objects will be attached using
* {@link SQLRelationship#attachRelationship(SQLTable, SQLTable, boolean)} rather than
* {@link SQLObject#addChild(SQLObject)}.
*
* @param parent The object whose children list to update
* @param newChildren The list of children to update from. All objects in this list
* are expected to be of the correct child type for parent. Also, they must not be attached
* to any parent objects to start with (this method may connect some or all of them).
* @param childType The type of child that this method should look for to update.
* @throws SQLObjectException If this exercise causes any SQLObjects to populate, and that
* populate operation fails.
*/
public static <T extends SQLObject> void refreshChildren(SQLObject parent, List<T> newChildren, Class<T> childType) throws SQLObjectException {
Set<String> oldChildNames = parent.getChildNames(childType);
Set<String> newChildNames = new HashSet<String>(); // will populate in following loop
for (T newChild : newChildren) {
newChildNames.add(newChild.getName());
if (oldChildNames.contains(newChild.getName())) {
parent.getChildByName(newChild.getName(), childType).updateToMatch(newChild);
} else {
if (newChild instanceof SQLRelationship) {
SQLRelationship r = (SQLRelationship) newChild;
r.attachRelationship(r.getPkTable(), r.getFkTable(), false);
} else {
parent.addChild(newChild);
}
}
}
// get rid of removed children
oldChildNames.removeAll(newChildNames);
for (String removedColName : oldChildNames) {
SQLObject removeMe = parent.getChildByName(removedColName, childType);
try {
parent.removeChild(removeMe);
} catch (IllegalArgumentException e) {
throw new SQLObjectException(e);
} catch (ObjectDependentException e) {
throw new SQLObjectException(e);
}
}
}
/**
* Creates a SQLTable in the given database, optionally under a catalog and/or schema.
*
* @param db The database to create the table in.
* @param catalog The catalog that the table (or the table's schema) should be in.
* If null, it is assumed the given database doesn't have catalogs.
* @param schema The schema that the table should be in. If null, it is assumed the
* given database doesn't have schemas.
* @param name The name of the table to create.
* @return The table that was created
* @throws SQLObjectException If you specify catalog or schema for a database that doesn't
* support catalogs or schemas; also if the database uses catalogs and schemas but you
* fail to provide them.
*/
public static SQLTable addSimulatedTable(SQLDatabase db, String catalog, String schema, String name) throws SQLObjectException {
if (db.getTableByName(catalog, schema, name) != null) {
throw new SQLObjectException("The table "+catalog+"."+schema+"."+name+" already exists");
}
SQLObject schemaContainer;
if (catalog != null) {
if (!db.isCatalogContainer()) {
throw new SQLObjectException("You tried to add a table with a catalog ancestor to a database that doesn't support catalogs.");
}
schemaContainer = db.getCatalogByName(catalog);
if (schemaContainer == null) {
schemaContainer = new SQLCatalog(db, catalog, true);
db.addChild(schemaContainer);
}
} else {
schemaContainer = db;
}
SQLObject tableContainer;
if (schema != null) {
if (!schemaContainer.allowsChildType(SQLSchema.class)) {
throw new SQLObjectException(
"The schema container ("+schemaContainer+
") can't actually contain children of type SQLSchema.");
}
tableContainer = schemaContainer.getChildByName(schema, SQLSchema.class);
if (tableContainer == null) {
tableContainer = new SQLSchema(schemaContainer, schema, true);
schemaContainer.addChild(tableContainer);
}
} else {
tableContainer = schemaContainer;
}
SQLTable newTable = new SQLTable(tableContainer, name, null, "TABLE", true);
tableContainer.addChild(newTable);
return newTable;
}
/**
* Recursively count tables in the project, but only consider tables that
* have been expanded.
*
* This might be undercounting a little bit because I think this suppresses
* the Target Database (playpen) entries.
*
* @param source the source object (usually the database)
*/
public static int countTables(SQLObject so) throws SQLObjectException {
if (so instanceof SQLTable) {
return 1;
} else if ( (!so.allowsChildren()) || !(so.isPopulated()) || so.getChildren() == null) {
return 0;
} else {
int myCount = 0;
for (SQLObject child : so.getChildren()) {
myCount += countTables(child);
}
return myCount;
}
}
/**
* Recursively count tables in the project, including ones that have not
* been expanded in the DBTree.
* <p>
* This has the side effect of populating all of the objects from the given
* object to the object containing {@link SQLTable}s.
*
* @param source
* the source object (usually the database)
*/
public static int countTablesSnapshot(SQLObject so) throws SQLObjectException {
if (so instanceof SQLTable) {
return 1;
} else {
int count = 0;
for (SQLObject child : so.getChildren()) {
count += countTablesSnapshot(child);
}
return count;
}
}
/**
* Searches for all SQLObjects under a given starting point which are of the
* given type. Keep in mind that if you go after anything lower than
* SQLTable in a lazy-loading database, you will invoke many potentially
* expensive populate() methods.
*
* @param source
* the source object (usually the database)
* @param clazz
* the type of SQLObject to look for.
* @param addTo
* the list to accumulate the results into. This list will likely
* be modified.
* @return the list passed in as the <code>addTo</code> argument.
*/
public static <T extends SQLObject>
List<T> findDescendentsByClass(SQLObject so, java.lang.Class<T> clazz, List<T> addTo)
throws SQLObjectException {
if (clazz == so.getClass()) {
addTo.add(clazz.cast(so));
} else {
for (SQLObject child : (List<? extends SQLObject>) so.getChildren()) {
findDescendentsByClass(child, clazz, addTo);
}
}
return addTo;
}
/**
* Searches for all columns in the target database which are marked as having
* source columns in the given source database.
*
* @param target The database to search. All columns of all tables in this database are searched.
* @param source The database to look for in the target database's columns.
* @return A list of all columns in the target database whose source database is the same
* as the given source object. Every item in the list will be of type SQLColumn.
*/
public static List<SQLColumn> findColumnsSourcedFromDatabase(SQLDatabase target, SQLDatabase source) throws SQLObjectException {
if (logger.isDebugEnabled()) logger.debug("Searching for dependencies on "+source+" in "+target);
List<SQLColumn> matches = new ArrayList<SQLColumn>();
Iterator<?> it = target.getChildren().iterator();
while (it.hasNext()) {
SQLObject so = (SQLObject) it.next();
if (logger.isDebugEnabled()) logger.debug("-->Next target item is "+so.getClass().getName()+": "+so+" ("+so.getChildCount()+" children)");
if (so instanceof SQLTable) {
SQLTable t = (SQLTable) so;
for (SQLColumn col : t.getColumns()) {
if (col.getSourceColumn() != null && source.equals(col.getSourceColumn().getParent().getParentDatabase())) {
matches.add(col);
}
}
}
}
return matches;
}
/**
* Finds the nearest common ancestor of all SQLObjects passed in. For
* example, if a bunch of columns from the same table are passed in, this
* method will return that table's columns folder. If a bunch of columns
* from different tables in the same schema are passed in, this method
* returns the database, catalog, or schema the tables belong to.
*
* @param items The items to find the common ancestor of
* @return
*/
public static SQLObject findCommonAncestor(Collection<? extends SQLObject> items) {
// first build up the full ancestory of one randomly chosen item
List<SQLObject> commonAncestors = ancestorList(items.iterator().next());
logger.debug("Initial ancestor list: " + commonAncestors);
// now prune the ancestor list to the largest common prefix with each item
for (SQLObject item : items) {
List<SQLObject> itemAncestors = ancestorList(item);
logger.debug(" Comparing with: " + itemAncestors);
Iterator<SQLObject> cit = commonAncestors.iterator();
Iterator<SQLObject> iit = itemAncestors.iterator();
while (cit.hasNext() && iit.hasNext()) {
if (cit.next() != iit.next()) {
cit.remove();
break;
}
}
// remove all remaining items in the common list because they're not in common with this item
while (cit.hasNext()) {
cit.next();
cit.remove();
}
logger.debug(" After this prune: " + commonAncestors);
}
SQLObject commonAncestor = commonAncestors.get(commonAncestors.size() - 1);
logger.debug("Returning: " + commonAncestor);
return commonAncestor;
}
/**
* Returns a list of SQLObjects where the first entity in the list is the
* highest ancestor and the last object in the list is the given object.
*/
public static List<SQLObject> ancestorList(SQLObject so) {
List<SQLObject> ancestors = new LinkedList<SQLObject>();
while (so != null) {
ancestors.add(0, so);
if (!(so.getParent() instanceof SQLObject)) break;
so = (SQLObject) so.getParent();
}
return ancestors;
}
/**
* Returns true if and only if the given set of arguments would result in a
* successful call to {@link addSimulatedTable}.
* See that method's documentation for the meaning of the arguments.
*
* @throws SQLObjectException if populating any of the relevant SQLObjects fails.
*/
public static boolean isCompatibleWithHierarchy(SQLDatabase db, String catalog, String schema, String name) throws SQLObjectException {
SQLObject schemaContainer;
if ( catalog != null){
if (db.isCatalogContainer()){
schemaContainer = db.getCatalogByName(catalog);
if (schemaContainer == null) {
return true;
}
} else {
return false;
}
} else {
schemaContainer = db;
}
SQLObject tableContainer;
if (schema != null){
if (schemaContainer.allowsChildType(SQLSchema.class)){
tableContainer = schemaContainer.getChildByName(schema, SQLSchema.class);
if (tableContainer == null) {
return true;
}
} else {
return false;
}
} else {
tableContainer = schemaContainer;
}
if (name != null) {
if (tableContainer.allowsChildType(SQLTable.class)) {
return true;
} else {
return false;
}
} else {
return false;
}
}
/**
* Returns the object that contains tables in the given database.
* Depending on platform, this could be a SQLDatabase, a SQLCatalog,
* or a SQLSchema. A null catName or schemName argument means that
* catalogs or schemas are not present in the given database.
* <p>
* Note, all comparisons are done case-insensitively.
*
* @param db The database to retrieve the table container from.
* @param catName The name of the catalog to retrieve. Must be null iff the
* database does not have catalogs.
* @param schemaName The name of the schema to retrieve. Must be null iff the
* database does not have schemas.
* @return The appropriate SQLObject under db that is a parent of SQLTable objects,
* given the catalog and schema name arguments.
* @throws SQLObjectException
*/
public static SQLObject getTableContainer(SQLDatabase db, String catName, String schemaName) throws SQLObjectException {
db.populate();
logger.debug("Looking for catalog="+catName+", schema="+schemaName+" in db "+db);
if (db.isTableContainer()) {
if (catName != null || schemaName != null) {
throw new IllegalArgumentException("Catalog or Schema name was given but neither is necessary.");
}
return db;
} else if (db.isSchemaContainer()) {
if (catName != null) {
throw new IllegalArgumentException("Catalog name was given but is not necessary.");
}
if (schemaName == null) {
throw new IllegalArgumentException("Schema name was expected but none was given.");
}
return (SQLSchema) db.getChildByNameIgnoreCase(schemaName, SQLSchema.class);
} else if (db.isCatalogContainer()) {
if (catName == null) {
throw new IllegalArgumentException("Catalog name was expected but none was given.");
}
SQLCatalog tempCat = db.getCatalogByName(catName);
if (tempCat == null) return null;
tempCat.populate();
logger.debug("Found catalog "+catName+". Child Type="+tempCat.getChildrenWithoutPopulating().get(0).getClass());
if (tempCat.isSchemaContainer()) {
if (schemaName == null) {
throw new IllegalArgumentException("Schema name was expected but none was given.");
}
return (SQLSchema) tempCat.getChildByNameIgnoreCase(schemaName, SQLSchema.class);
}
if (schemaName != null) {
throw new IllegalArgumentException("Schema name was given but is not necessary.");
}
return tempCat;
} else if (db.getChildrenWithoutPopulating().isEmpty()) {
// special case: there are no children of db
logger.debug("Database "+db+" has no children");
return null;
} else {
throw new IllegalStateException("Unknown database child type: " + db.getChildrenWithoutPopulating().get(0).getClass());
}
}
/**
* Populates the given object by adding all of the children in the list to
* the object and setting necessary flags. This method must be called on the
* foreground and the object cannot be populated already.
*
* @param objectToPopulate
* The object to populate with the given child types.
* @param children
* A list of children that will be used to populate the given
* object. The object will be defined to be populated for the
* child type in the list. The list can contain multiple child
* types but must contain all of the children of any one type to
* fully populate that type of children for the object.
*/
public static void populateChildrenWithList(SQLObject objectToPopulate, List<SQLObject> children) {
if (objectToPopulate instanceof SQLTable) {
List<SQLColumn> columnChildren = new ArrayList<SQLColumn>();
List<SQLIndex> indexChildren = new ArrayList<SQLIndex>();
List<SQLRelationship> relationshipChildren = new ArrayList<SQLRelationship>();
List<SQLImportedKey> importedKeyChildren = new ArrayList<SQLImportedKey>();
for (SQLObject o : children) {
if (o instanceof SQLColumn) {
columnChildren.add((SQLColumn) o);
} else if (o instanceof SQLIndex) {
indexChildren.add((SQLIndex) o);
} else if (o instanceof SQLRelationship) {
relationshipChildren.add((SQLRelationship) o);
} else if (o instanceof SQLImportedKey) {
importedKeyChildren.add((SQLImportedKey) o);
} else {
throw new IllegalArgumentException("Unknown child type " + o.getClass() +
" for table " + objectToPopulate + ". Cannot add " + o + " as a child.");
}
}
if (!columnChildren.isEmpty()) {
SQLTable.populateColumnsWithList((SQLTable) objectToPopulate, columnChildren);
}
if (!indexChildren.isEmpty()) {
SQLTable.populateIndicesWithList((SQLTable) objectToPopulate, indexChildren);
}
if (!relationshipChildren.isEmpty()) {
SQLTable.populateRelationshipsWithList((SQLTable) objectToPopulate, relationshipChildren);
}
//Imported keys do not follow the normal pattern as they are not actually
//populated by calling populate().
//TODO come up with a better way to satisfy handling imported keys
//that still doesn't cause a tightly connected graph of tables to be
//fully populated when populating one table.
if (!importedKeyChildren.isEmpty()) {
for (SQLImportedKey child : importedKeyChildren) {
((SQLTable) objectToPopulate).addImportedKey(child);
}
((SQLTable) objectToPopulate).setImportedKeysPopulated(true);
}
} else if (objectToPopulate instanceof SQLDatabase) {
SQLDatabase.populateDatabaseWithList((SQLDatabase) objectToPopulate, children);
} else if (objectToPopulate instanceof SQLCatalog) {
SQLCatalog.populateCatalogWithList((SQLCatalog) objectToPopulate, children);
} else if (objectToPopulate instanceof SQLSchema) {
List<SQLTable> tables = new ArrayList<SQLTable>();
for (SQLObject child : children) {
tables.add((SQLTable) child);
}
SQLSchema.populateSchemaWithList((SQLSchema) objectToPopulate, tables);
} else {
throw new IllegalArgumentException("Object " + objectToPopulate + " does not " +
"allow children and cannot be populated.");
}
}
}