/* * 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.SQLException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.lang.NotImplementedException; import org.apache.log4j.Logger; import ca.sqlpower.object.AbstractSPObject; import ca.sqlpower.object.ObjectDependentException; import ca.sqlpower.object.SPListener; import ca.sqlpower.object.SPObject; import ca.sqlpower.object.annotation.Accessor; import ca.sqlpower.object.annotation.Mutator; import ca.sqlpower.object.annotation.NonProperty; import ca.sqlpower.object.annotation.Transient; import ca.sqlpower.sql.jdbcwrapper.DatabaseMetaDataDecorator; import ca.sqlpower.util.SQLPowerUtils; import com.google.common.collect.ListMultimap; /** * SQLObject is the main base class of the Architect API. All objects that can * be reverse-engineered from or forward-engineered to an SQL database are * represented as SQLObject subclasses. The main features inherited from * SQLObject are: * * <h2>Tree structure</h2> * * SQLObjects are arranged in a tree structure: each object has a parent, which * is also a SQLObject, and it has a list of children which point back to it. * All children of any given SQLObject must be of the exact same type. This is * enforced in several places, so you should find out quickly if you break this * rule. * * <h2>Transparent lazy reverse engineering</h2> * * SQLObjects have two primary states: populated and unpopulated. The state * transitions from unpopulated to populated when the child list is filled in by * reverse engineering the information from a physical SQL database. The state * never transitions from populated to unpopulated. * <p> * When creating a SQLObject, you can decide whether you want it to start in the * populated state or not. When starting in the populated state, the lazy * reverse engineering feature will not be active, and the SQLObject can (must) * be completely configured via its API. * * <h2>Event System</h2> * * Most changes to the state of a SQLObject cause an event to be fired. This is * useful when building GUI components and undo/redo systems around SQLObjects. * See {@link SQLObjectEvent} and {@link SQLObjectListener} for details. * * <h2>Client Properties</h2> * * Every SQLObject maintains a map of key/value pairs. This map is segregated * into namespaces to ensure multiple clients who don't know about each other do * not end up suffering naming collisions. */ public abstract class SQLObject extends AbstractSPObject implements java.io.Serializable { private static Logger logger = Logger.getLogger(SQLObject.class); protected boolean populated = false; private AtomicBoolean populating = new AtomicBoolean(false); /** * The name used for this object in a physical database system. This name may have * to be altered to fit the naming constraints of a particular system, in terms * of length, case, allowable characters, and other requirements. */ private String physicalName; /** * The map that hold the client properties of this object. Don't modify the * contents of this map directly; use the {@link #putClientProperty(Class, String, Object)} * and {@link #getClientProperty(Class, String)} methods which take care of * firing events and other such bookkeeping. */ private final Map<String, Object> clientProperties = new HashMap<String, Object>(); /** * This is the throwable that tells if the children of this component can be * reached or not. If the exception at the child type is null then the * children can be reached. If it is not null then there was an exception * the last time the children were attempted to be accessed. There can also * be an exception at SQLObject itself if an exception occurred that * prevented the children from populating and was either not tied to a * specific child type or not enough information is known to tell which * child type it actually failed on. */ private final Map<Class<? extends SQLObject>, Throwable> childrenInaccessibleReason = new HashMap<Class<? extends SQLObject>, Throwable>(); /** * Returns the name used for this object in a physical database system. This * name may have to be altered to fit the naming constraints of a particular * system, in terms of length, case, allowable characters, duplication of * names within the same namespace, and other requirements. Presently, the * DDL Generators take it upon themselves to perform this alteration. In the * near future, we hope to make the DDL Generators use this name verbatim * and simply fail on names that are not permissible. The responsibility of * suggesting physical name changes will shift to critics configured for * each database platform's particular needs. Those critics would provide * "quick fix" suggestions with names that are legal in their own target * platform, and then set them. * <p> * there is no good reason why this method is declared final, but there is * no good reason to override it at this time. * * @return The physical name to use for forward engineering and database * comparison, or the logical name (see {@link #getName()}) if no * physical name has been set. */ @Accessor(isInteresting=true) public final String getPhysicalName() { if (physicalName != null) { return physicalName; } return getName(); } /** * Sets the physical identifier name to use when forward-engineering into * the target database and comparing with existing databases. * * @param argName The new physical name to use. */ @Mutator public void setPhysicalName(String argName) { String oldPhysicalName = getPhysicalName(); String actualOldPhysicalName = physicalName; this.physicalName = argName; //The old physicalName returned from getPhysicalName must be the same //as that returned by getPhysicalName or the persisters will fail. However, //if the physical name is being set to null when it was null we do not want //to fire an event. if ((actualOldPhysicalName == null && argName == null) || (actualOldPhysicalName != null && actualOldPhysicalName.equals(argName))) return; firePropertyChange("physicalName",oldPhysicalName,argName); } /** * Causes this SQLObject to load its children through populateImpl (if any exist). * This will do nothing if the object is already populated. */ public final synchronized void populate() throws SQLObjectException { if (populated || !populating.compareAndSet(false, true)) return; // We're going to just leave caching on all the time and see how it pans out DatabaseMetaDataDecorator.putHint( DatabaseMetaDataDecorator.CACHE_TYPE, DatabaseMetaDataDecorator.CacheType.EAGER_CACHE); childrenInaccessibleReason.clear(); try { populateImpl(); } catch (final SQLObjectException e) { runInForeground(new Runnable() { public void run() { try { setChildrenInaccessibleReason(e, SQLObject.class, true); } catch (SQLObjectException e) { throw new RuntimeException(e); } } }); } catch (final RuntimeException e) { runInForeground(new Runnable() { public void run() { try { setChildrenInaccessibleReason(e, SQLObject.class, true); } catch (SQLObjectException e) { throw new RuntimeException(e); } } }); } finally { populating.set(false); } } /** * Causes this SQLObject to load its children (if any exist). * This method will be called lots of times, so track whether or * not you need to do anything and return right away whenever * possible. */ protected abstract void populateImpl() throws SQLObjectException; /** * Returns a short string that should be displayed to the user for * representing this SQLObject as a label. */ @Transient @Accessor public abstract String getShortDisplayName(); /** * Tells if this object has already been filled with children, or * if that operation is still pending. */ @Accessor public boolean isPopulated() { return populated; } /** * Lets outside users modify the internal flag that says whether * or not the list of child objects has already been loaded from * the source database. Users of this SQLObject hierarchies should * not normally call this method, but it needs to be public for the * SwingUIProject load implementation. */ @Mutator public void setPopulated(boolean v) { boolean oldPop = populated; populated = v; firePropertyChange("populated", oldPop, v); } /** * Returns true if and only if this object can have child * SQLObjects. Your implementation of this method <b>must not</b> * cause JDBC activity, or the lazy loading properties of your * SQLObjects will be wasted! */ @Override public boolean allowsChildren() { return super.allowsChildren(); } @Override public boolean removeChild(SPObject child) throws ObjectDependentException, IllegalArgumentException { if (child instanceof SQLObject) { if (!fireDbChildPreRemove(getChildrenWithoutPopulating(child.getClass()).indexOf(child), (SQLObject) child)) { return false; } } if (!getChildrenWithoutPopulating().contains(child)) { return false; } return removeChildImpl(child); } /** * Returns an unmodifiable view of the child list. All list * members will be SQLObject subclasses (SQLTable, * SQLRelationship, SQLColumn, etc.) which are directly contained * within this SQLObject. */ @NonProperty public List<? extends SQLObject> getChildren() { return getChildren(SQLObject.class); } @NonProperty public <T extends SPObject> List<T> getChildren(Class<T> type) { try { if (isMagicEnabled()) { populate(); } return getChildrenWithoutPopulating(type); } catch (SQLObjectException e) { throw new RuntimeException("Could not populate " + getName(), e); } } /** * Returns a new and unmodifiable list of all SQLObjects currently children * of this object. The list of objects is unmodifiable as children cannot * be added or removed through it. The list is a new list instead of wrapping * the list in an unmodifiable list to let the list be updated on one thread * while it is being iterated over on another thread. * <p> * Calling this method will not cause the object to populate. * @return */ @NonProperty public abstract List<? extends SQLObject> getChildrenWithoutPopulating(); @NonProperty public <T extends SPObject> List<T> getChildrenWithoutPopulating(Class<T> type) { List<T> children = new ArrayList<T>(); for (SQLObject child : getChildrenWithoutPopulating()) { if (type.isAssignableFrom(child.getClass())) { children.add(type.cast(child)); } } return Collections.unmodifiableList(children); } @NonProperty public SQLObject getChild(int index) throws SQLObjectException { populate(); return (SQLObject) getChildrenWithoutPopulating().get(index); } @NonProperty public int getChildCount() throws SQLObjectException { populate(); return getChildrenWithoutPopulating().size(); } /** * Returns the names of all children of this SQLObject. Causes this * SQLObject to become populated. * <p> * Originally created for internal use during refresh. There should be no * harm in making this method public if it's needed externally. * * @throws SQLObjectException * if populating this object fails */ @NonProperty Set<String> getChildNames() throws SQLObjectException { return getChildNames(SQLObject.class); } /** * Returns the names of all children of a certain type of this SQLObject. * Causes this SQLObject to become populated. * <p> * Originally created for internal use during refresh. There should be no * harm in making this method public if it's needed externally. * * @throws SQLObjectException * if populating this object fails */ @NonProperty <T extends SQLObject> Set<String> getChildNames(Class<T> childType) { HashSet<String> names = new HashSet<String>(); for (T child : getChildren(childType)) { names.add(child.getName()); } return names; } /** * Adds the given SQLObject to this SQLObject at index. Causes a * DBChildrenInserted event. If you want to override the * behaviour of addChild, override this method. * @throws SQLObjectException */ // public void addChild(SQLObject newChild, int index) throws SQLObjectException { // addChildImpl(newChild, index); // } /** * Adds the given SQLObject to this SQLObject at the end of the * child list by calling {@link #addChild(SPObject, int)}. Causes * a DBChildrenInserted event. If you want to override the * behaviour of addChild, do not override this method. * @throws SQLObjectException */ public void addChild(SQLObject newChild) throws SQLObjectException { addChild(newChild, getChildrenWithoutPopulating(newChild.getClass()).size()); } // ------------------- sql object event support ------------------- /* * @return An immutable copy of the list of SQLObject listeners */ @NonProperty public List<SPListener> getSPListeners() { return listeners; } // ------------------- sql object Pre-event support ------------------- private final transient List<SQLObjectPreEventListener> sqlObjectPreEventListeners = new ArrayList<SQLObjectPreEventListener>(); /** * @return An immutable copy of the list of SQLObject pre-event listeners */ @NonProperty public List<SQLObjectPreEventListener> getSQLObjectPreEventListeners() { return sqlObjectPreEventListeners; } public void addSQLObjectPreEventListener(SQLObjectPreEventListener l) { if (l == null) throw new NullPointerException("You can't add a null listener"); synchronized(sqlObjectPreEventListeners) { if (sqlObjectPreEventListeners.contains(l)) { if (logger.isDebugEnabled()) { logger.debug("NOT Adding duplicate pre-event listener "+l+" to SQLObject "+this); } return; } sqlObjectPreEventListeners.add(l); } } public void removeSQLObjectPreEventListener(SQLObjectPreEventListener l) { synchronized(sqlObjectPreEventListeners) { sqlObjectPreEventListeners.remove(l); } } /** * Fires a pre-remove event, and returns the status of whether or not the * operation should proceed. * * @param oldIndices The child indices that might be removed * @param oldChildren The children that might be removed * @return True if the operation should proceed; false if it should not. */ protected boolean fireDbChildrenPreRemove(int[] oldIndices, List<SQLObject> oldChildren) { if (logger.isDebugEnabled()) { logger.debug(getClass().getName()+" "+toString()+": " + "firing dbChildrenPreRemove event"); } SQLObjectPreEvent e = new SQLObjectPreEvent (this, oldIndices, (SQLObject[]) oldChildren.toArray(new SQLObject[oldChildren.size()])); int count = 0; synchronized (sqlObjectPreEventListeners) { SQLObjectPreEventListener[] listeners = sqlObjectPreEventListeners.toArray(new SQLObjectPreEventListener[0]); for (SQLObjectPreEventListener l : listeners) { l.dbChildrenPreRemove(e); count++; } } if (logger.isDebugEnabled()) logger.debug("Notified "+count+" listeners. Veto="+e.isVetoed()); return !e.isVetoed(); } /** * Convenience method for {@link #fireDbChildrenPreRemove(int[], List)} when there * is only one child being removed. * * @param oldIndex The index of the child to be removed * @param oldChild The child to be removed */ protected boolean fireDbChildPreRemove(int oldIndex, SQLObject oldChild) { int[] oldIndexArray = new int[1]; oldIndexArray[0] = oldIndex; List<SQLObject> oldChildList = new ArrayList<SQLObject>(1); oldChildList.add(oldChild); return fireDbChildrenPreRemove(oldIndexArray, oldChildList); } @NonProperty public <T extends SQLObject> T getChildByName(String name, Class<T> childType) { return getChildByNameImpl(name, false, childType); } @NonProperty public <T extends SQLObject> T getChildByNameIgnoreCase(String name, Class<T> childType) { return getChildByNameImpl(name, true, childType); } /** * Searches for a child object based on its class type, name and case * sensitivity. * * @param <T> * The child type of SQLObject to look for * @param name * The name of the child * @param ignoreCase * Whether the name search should be case sensitive * @param childType * The child type to look for when searching for the SQLObject's * name * @return The found child with the given name, or null if it does not exist. */ @NonProperty private <T extends SQLObject> T getChildByNameImpl(String name, boolean ignoreCase, Class<T> childType) { for (T o : getChildren(childType)) { if ( (ignoreCase && o.getName().equalsIgnoreCase(name)) || ( (!ignoreCase) && o.getName().equals(name)) ) { return o; } } return null; } /** * Returns the index of the named child, or -1 if there is no child with * that name. * * @param name The name of the child to look for (case sensitive) * @return The index of the named child in the child list, or -1 if there * is no such child. * @throws SQLObjectException if the child list can't be populated */ @NonProperty public int getIndexOfChildByName(String name) throws SQLObjectException { int i = 0; for (Object o : getChildren()) { SQLObject so = (SQLObject) o; if (so.getName().equals(name)) { return i; } i++; } return -1; } /** * Sets the current value of the named client property in the given * namespace. If the new value is different from the existing value, * a SQLObjectChangedEvent will be fired with the property name * of <code>namespace.getName() + "." + propName</code>. * * @param namespace * The namespace to look in. This is usually the class of the * code calling in, but there is no restriction from getting and * setting client properties maintained by other classes. * @param propName * The name of the property to set. */ public void putClientProperty(Class<?> namespace, String propName, Object property) { String key = namespace + "." + propName; Object oldValue = clientProperties.get(key); clientProperties.put(key, property); firePropertyChange("clientProperty." + key, oldValue, property); } /** * Returns the current value of the named client property in the given * namespace. * * @param namespace * The namespace to look in. This is usually the class of the * code calling in, but there is no restriction from getting and * setting client properties maintained by other classes. * @param propName * The name of the property to get. * @return The property's current value, or null if the property is not set * on this SQL Object. */ @NonProperty public Object getClientProperty(Class<?> namespace, String propName) { return clientProperties.get(namespace + "." + propName); } /** * Rerturns the property names of all client properties currently set * on this SQLObject. */ @NonProperty public Set<String> getClientPropertyNames() { return clientProperties.keySet(); } @Transient @Accessor public Throwable getChildrenInaccessibleReason(Class<? extends SQLObject> childType) { return childrenInaccessibleReason.get(childType); } @Transient @Accessor public Map<Class<? extends SQLObject>, Throwable> getChildrenInaccessibleReasons() { return Collections.unmodifiableMap(childrenInaccessibleReason); } /** * This setter will take in a Throwable to set the inaccessible reason to, * for things like copy methods. See {@link SQLObject#childrenInaccessibleReason}; * * @param cause * The throwable that made the children of this object * inaccessible * @param childType * The type of child that is inaccessible. Can be * {@link SQLObject} if the exception covers all of the children. * @param rethrow * Decides if the cause should be rethrown wrapped in a * SQLObjectException. Set this to true to have the exception be * rethrown. */ @Transient @Mutator public void setChildrenInaccessibleReason(Throwable cause, Class<? extends SQLObject> childType, boolean rethrow) throws SQLObjectException { Map<Class<? extends SQLObject>, Throwable> oldVal = new HashMap<Class<? extends SQLObject>, Throwable>(this.childrenInaccessibleReason); this.childrenInaccessibleReason.put(childType, cause); firePropertyChange("childrenInaccessibleReason", oldVal, childrenInaccessibleReason); setPopulated(true); if (rethrow) { if (cause instanceof SQLObjectException) { throw (SQLObjectException) cause; } else if (cause instanceof RuntimeException) { throw (RuntimeException) cause; } else { throw new SQLObjectException(cause); } } } /** * A basic refresh method that simply calls refresh on all existing * children. Most classes will have to override this with an implementation * that checks for changes in the physical database and adjusts the child * list accordingly. * <p> * Note that this package-private method is not meant to be called directly! * All refresh operations have to be initiated by calling the parent database's * refresh method, {@link SQLDatabase#refresh()}, which is public. */ void refresh() throws SQLObjectException { if (!isPopulated()) { logger.debug("Not refreshing unpopulated object " + this); return; } if (isTableContainer()) { logger.debug("Refreshing table container " + this); Connection con = null; SQLDatabase db = SQLPowerUtils.getAncestor(this, SQLDatabase.class); try { SQLCatalog cat = SQLPowerUtils.getAncestor(this, SQLCatalog.class); SQLSchema sch = SQLPowerUtils.getAncestor(this, SQLSchema.class); String catName = cat == null ? null : cat.getName(); String schName = sch == null ? null : sch.getName(); con = db.getConnection(); DatabaseMetaData dbmd = con.getMetaData(); final List<SQLTable> newChildren = SQLTable.fetchTablesForTableContainer( dbmd, catName, schName); runInForeground(new Runnable() { public void run() { try { SQLObjectUtils.refreshChildren(SQLObject.this, newChildren, SQLTable.class); } catch (SQLObjectException e) { throw new SQLObjectRuntimeException(e); } } }); try { final ListMultimap<String, SQLColumn> newCols = SQLColumn.fetchColumnsForTable( catName, schName, null, dbmd); runInForeground(new Runnable() { public void run() { List<SQLTable> populatedTables = new ArrayList<SQLTable>(); for (SQLTable table : getChildrenWithoutPopulating(SQLTable.class)) { if (table.isColumnsPopulated()) { populatedTables.add(table); } } for (SQLTable table : populatedTables) { try { SQLObjectUtils.refreshChildren(table, newCols.get(table.getName()), SQLColumn.class); } catch (SQLObjectException e) { throw new SQLObjectRuntimeException(e); } } } }); } catch (SQLException e) { throw new SQLObjectException("Refresh failed", e); } for (SQLTable t : getChildrenWithoutPopulating(SQLTable.class)) { t.refreshIndexes(); } for (SQLTable t : getChildrenWithoutPopulating(SQLTable.class)) { t.refreshExportedKeys(); } logger.debug("Table container refresh complete for " + this); } catch (SQLException e) { throw new SQLObjectException("Refresh failed", e); } finally { try { if (con != null) con.close(); } catch (SQLException ex) { logger.warn("Failed to close connection! Squishing this exception:", ex); } } } else { for (SQLObject o : getChildrenWithoutPopulating()) { o.refresh(); } } } /** * Returns true if this SQLObject is definitely a container for SQLTable * objects. Depending on the source database topology, instances of * SQLDatabase, SQLCatalog, and SQLSchema may return true. Other types of * SQLObject will always return false, since there is no topology in which * they are table containers. Calling this method will never result in * populating an unpopulated SQLObject. * <p> * If this SQLObject is populated and has at least one child, this method * makes the determination cheap and accurate by checking if the children * are of type SQLTable. Otherwise, the object has no children (whether or * not it is populated), so this method examines the JDBC driver's database * metadata to determine the topology based on the reported catalogTerm and * schemaTerm. A null value for either term is interpreted to mean the * database does not have that level of object containment. The (major) * downside of this approach is that it does not work when the database * connection is unavailable. * * @return * @throws SQLObjectException * if the determination requires database metadata access, and * it's not possible to obtain the database connection or the * database metadata. */ @NonProperty public boolean isTableContainer() throws SQLObjectException { // first, check for existing SQLTable children--this is a dead giveaway for a table container! if (getChildrenWithoutPopulating().size() > 0) { return (getChildrenWithoutPopulating(SQLTable.class).size() != 0); } // no children. we have to do a bit of structural investigation. // schemas can only contain tables if (getClass() == SQLSchema.class) { return true; } // determination for catalogs and databases requires database metadata Connection con = null; try { // catalogs could contain schemas or tables. If schemaTerm is null, it must be tables. if (getClass() == SQLCatalog.class) { SQLDatabase db = (SQLDatabase) getParent(); con = db.getConnection(); if (con == null) { throw new SQLObjectException("Unable to determine table container status without database connection"); } DatabaseMetaData dbmd = con.getMetaData(); return dbmd.getSchemaTerm() == null; } // databases could contain catalogs, schemas, or tables if (getClass() == SQLDatabase.class) { SQLDatabase db = (SQLDatabase) this; con = db.getConnection(); if (con == null) { throw new SQLObjectException("Unable to determine table container status without database connection"); } DatabaseMetaData dbmd = con.getMetaData(); return (dbmd.getSchemaTerm() == null) && (dbmd.getCatalogTerm() == null); } } catch (SQLException ex) { throw new SQLObjectException("Failed to obtain database metadata", ex); } finally { try { if (con != null) con.close(); } catch (SQLException ex) { logger.warn("Failed to close connection", ex); } } // other types of SQLObject are never table containers return false; } /** * Updates all the properties of this SQLObject to match those of the other * SQLObject. Some implementations also update the list of children (see * SQLIndex and SQLRelationship for examples of this). If any of the properties * of this object change as a result of the update, the corresponding * events will be fired. * * @param source * The SQLObject to read the new property values from. Must be of * the same type as the receiving object. * @throws SQLObjectException if the attempted update causes a populate that fails * @throws NotImplementedException * The default implementation from SQLObject just throws this * exception. Subclasses that want to implement this * functionality must override this method. * @throws ClassCastException * if the given source object is not the same type as this * SQLObject. */ public void updateToMatch(SQLObject source) throws SQLObjectException { throw new UnsupportedOperationException(); } /** * Updates the physical name to be the same as the logical name if the * current physical name and the old logical name match or if the physical * name is missing. * * @param oldName * The name before the latest name change. Used to check if the * physical name is a match. * @param newName * The name the logical name is being changed to. If the old name * and physical name match the physical name will be set to this * one. */ protected void updatePhysicalNameToMatch(String oldName, String newName) { if ((newName != null && getPhysicalName() == null) || (getPhysicalName() != null && "".equals(getPhysicalName().trim())) || (oldName != null && oldName.equals(getPhysicalName()))) { setPhysicalName(newName); } } }