Author: jfuerth
Date: Thu Oct 16 15:45:43 2008
New Revision: 2785
Modified:
trunk/regress/ca/sqlpower/architect/SQLObjectTest.java
trunk/regress/ca/sqlpower/architect/profile/TestProfileManager.java
trunk/src/ca/sqlpower/architect/SQLObject.java
trunk/src/ca/sqlpower/architect/profile/ProfileManager.java
trunk/src/ca/sqlpower/architect/profile/ProfileManagerImpl.java
trunk/src/ca/sqlpower/architect/profile/event/ProfileChangeEvent.java
trunk/src/ca/sqlpower/architect/swingui/dbtree/DBTreeModel.java
Log:
Got rid of a hideous secret in the DBTreeModel: it was actually gluing the
profile manager to the SQLObjects it had profiles for!
We solved this problem by introducing "client properties" to SQLObject,
which serve much the same purpose as client properties of JComponents. Now
anything can stash any number of named properties into a SQLObject, and get
them back later. Modifying a client property causes a
SQLObjectChangedEvent, which was the whole reason for the hideous code in
DBTreeModel in the first place. Client properties are not saved with the
project, so they are only useful for storing transient or redundant
information.
For our initial use of client properties, we now have the ProfileManager
storing a count of the number of profiles associated with each SQLTable.
This allows the DBTree to notice a normal property change on a tree node
and repaint it in the usual way. The DBTreeModel now has no knowledge of
the profile manager, which is a good thing.
Modified: trunk/regress/ca/sqlpower/architect/SQLObjectTest.java
==============================================================================
--- trunk/regress/ca/sqlpower/architect/SQLObjectTest.java (original)
+++ trunk/regress/ca/sqlpower/architect/SQLObjectTest.java Thu Oct 16
15:45:43 2008
@@ -247,5 +247,17 @@
assertEquals("Event fired", 1, l.getPreRemoveCount());
assertEquals("Child not removed", 1, target.getChildren().size());
}
+
+ public void testClientPropertySetAndGet() {
+ target.putClientProperty(this.getClass(), "testProperty", "test
me");
+ assertEquals("test me",
target.getClientProperty(this.getClass(), "testProperty"));
+ }
+
+ public void testClientPropertyFiresEvent() {
+ CountingSQLObjectListener listener = new
CountingSQLObjectListener();
+ target.addSQLObjectListener(listener);
+ target.putClientProperty(this.getClass(), "testProperty", "test
me");
+ assertEquals(1, listener.getChangedCount());
+ }
}
Modified:
trunk/regress/ca/sqlpower/architect/profile/TestProfileManager.java
==============================================================================
--- trunk/regress/ca/sqlpower/architect/profile/TestProfileManager.java
(original)
+++ trunk/regress/ca/sqlpower/architect/profile/TestProfileManager.java Thu
Oct 16 15:45:43 2008
@@ -130,4 +130,23 @@
t1Results = pm.getResults(t1);
assertEquals(0, t1Results.size());
}
+
+ public void testProfilingSetsClientProperty() throws Exception {
+ //Setup already created a profile.
+ assertEquals(1, t1.getClientProperty(ProfileManager.class,
ProfileManager.PROFILE_COUNT_PROPERTY));
+ }
+
+ public void testRemovingProfileSetsClientProperty() throws Exception {
+ List<TableProfileResult> profiles = pm.getResults(t1);
+ for (TableProfileResult tpr : profiles) {
+ pm.removeProfile(tpr);
+ }
+ assertEquals(0, t1.getClientProperty(ProfileManager.class,
ProfileManager.PROFILE_COUNT_PROPERTY));
+ }
+
+ public void testProfilingTableContainerSetsClientProperty() throws
Exception {
+ //Setup already created a profile.
+ assertEquals(1, t1.getClientProperty(ProfileManager.class,
ProfileManager.PROFILE_COUNT_PROPERTY));
+ }
+
}
Modified: trunk/src/ca/sqlpower/architect/SQLObject.java
==============================================================================
--- trunk/src/ca/sqlpower/architect/SQLObject.java (original)
+++ trunk/src/ca/sqlpower/architect/SQLObject.java Thu Oct 16 15:45:43 2008
@@ -20,8 +20,11 @@
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import org.apache.log4j.Logger;
@@ -30,6 +33,44 @@
import ca.sqlpower.architect.undo.UndoCompoundEventListener;
import ca.sqlpower.architect.undo.UndoCompoundEvent.EventTypes;
+/**
+ * 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 [EMAIL PROTECTED] SQLObjectEvent} and [EMAIL PROTECTED]
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 implements java.io.Serializable {
private static Logger logger = Logger.getLogger(SQLObject.class);
@@ -40,6 +81,14 @@
private String name;
/**
+ * The map that hold the client properties of this object. Don't modify
the
+ * contents of this map directly; use the [EMAIL PROTECTED]
#putClientProperty(Class, String, Object)}
+ * and [EMAIL PROTECTED] #getClientProperty(Class, String)} methods which take care
of
+ * firing events and other such bookkeeping.
+ */
+ private final Map<String, Object> clientProperties = new HashMap<String,
Object>();
+
+ /**
* The children of this SQLObject (if not applicable, set to
* Collections.EMPTY_LIST in your constructor).
*/
@@ -640,5 +689,50 @@
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);
+ fireDbObjectChanged("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.
+ */
+ 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.
+ */
+ public Set<String> getClientPropertyNames() {
+ return clientProperties.keySet();
}
}
Modified: trunk/src/ca/sqlpower/architect/profile/ProfileManager.java
==============================================================================
--- trunk/src/ca/sqlpower/architect/profile/ProfileManager.java (original)
+++ trunk/src/ca/sqlpower/architect/profile/ProfileManager.java Thu Oct 16
15:45:43 2008
@@ -35,6 +35,12 @@
public interface ProfileManager {
/**
+ * A client property defined on an object to count how many profiles
+ * of the SQLObject this profile manager currently holds.
+ */
+ public String PROFILE_COUNT_PROPERTY = "profileCount";
+
+ /**
* Returns an unmodifiable snapshot of the list of all
TableProfileResults that this
* ProfileManager keeps track of. You may get multiple ProfileResults
* for one table since you are allowed to have multiple profiles of a
Modified: trunk/src/ca/sqlpower/architect/profile/ProfileManagerImpl.java
==============================================================================
--- trunk/src/ca/sqlpower/architect/profile/ProfileManagerImpl.java
(original)
+++ trunk/src/ca/sqlpower/architect/profile/ProfileManagerImpl.java Thu Oct
16 15:45:43 2008
@@ -104,7 +104,9 @@
private final List<ProfileChangeListener> profileChangeListeners = new
ArrayList<ProfileChangeListener>();
/**
- * All profile results in this profile manager.
+ * All profile results in this profile manager. IMPORTANT: Do not
modify this list
+ * directly. Always use [EMAIL PROTECTED] #addResults(List)},
+ * [EMAIL PROTECTED] #removeResults(List)}, and [EMAIL PROTECTED]
#clear()}.
*/
private final List<TableProfileResult> results = new
ArrayList<TableProfileResult>();
@@ -177,11 +179,25 @@
}
}
+ /**
+ * This is the method that everything which wants to add a profile
result
+ * must call in order to add the result. It takes care of setting
SQLObject
+ * client properties, firing events, and actually adding the profile
to the
+ * set of profile results in this profile manager.
+ */
+ private void addResults(List<TableProfileResult> newResults) {
+ results.addAll(newResults);
+ fireProfilesAdded(newResults);
+ for (TableProfileResult newResult : newResults) {
+ SQLTable table = newResult.getProfiledObject();
+ table.putClientProperty(ProfileManager.class,
PROFILE_COUNT_PROPERTY, getResults(table).size());
+ }
+ }
+
/* docs inherited from interface */
public TableProfileResult createProfile(SQLTable table) throws
ArchitectException {
TableProfileResult tpr = new TableProfileResult(table,
getDefaultProfileSettings());
- results.add(tpr);
- fireProfileAdded(tpr);
+ addResults(Collections.singletonList(tpr));
try {
profileExecutor.submit(new ProfileResultCallable(tpr)).get();
@@ -203,8 +219,7 @@
profiles.add(new TableProfileResult(t,
getDefaultProfileSettings()));
}
- results.addAll(profiles);
- fireProfilesAdded(profiles);
+ addResults(profiles);
List<Future<TableProfileResult>> results = new
ArrayList<Future<TableProfileResult>>();
for (TableProfileResult tpr : profiles) {
@@ -223,6 +238,10 @@
List<TableProfileResult> oldResults = new
ArrayList<TableProfileResult>(results);
results.clear();
fireProfilesRemoved(oldResults);
+ for (TableProfileResult oldResult : oldResults) {
+ SQLTable table = oldResult.getProfiledObject();
+ table.putClientProperty(ProfileManager.class,
PROFILE_COUNT_PROPERTY, 0);
+ }
}
/* docs inherited from interface */
@@ -246,8 +265,10 @@
public boolean removeProfile(TableProfileResult victim) {
boolean removed = results.remove(victim);
if (removed) {
- fireProfileRemoved(victim);
+ fireProfilesRemoved(Collections.singletonList(victim));
}
+ SQLTable table = victim.getProfiledObject();
+ table.putClientProperty(ProfileManager.class,
PROFILE_COUNT_PROPERTY, getResults(table).size());
return removed;
}
@@ -292,8 +313,7 @@
public void loadResult(ProfileResult pr) {
if (pr instanceof TableProfileResult) {
TableProfileResult tpr = (TableProfileResult) pr;
- results.add(tpr);
- fireProfileAdded(tpr);
+ addResults(Collections.singletonList(tpr));
}
// the column results will get added to the table result by
// the project's profile result factory class
@@ -316,17 +336,6 @@
}
/**
- * Creates and fires a "profilesAdded" event for the given profile
result.
- */
- private void fireProfileAdded(TableProfileResult tpr) {
- if (tpr == null) throw new NullPointerException("Can't fire event
for adding null profile");
- ProfileChangeEvent e = new ProfileChangeEvent(this, tpr);
- for (int i = profileChangeListeners.size() - 1; i >= 0; i--) {
- profileChangeListeners.get(i).profilesAdded(e);
- }
- }
-
- /**
* Creates and fires a "profilesAdded" event for the given profile
results.
*/
private void fireProfilesAdded(List<TableProfileResult> results) {
@@ -334,17 +343,6 @@
ProfileChangeEvent e = new ProfileChangeEvent(this, results);
for (int i = profileChangeListeners.size() - 1; i >= 0; i--) {
profileChangeListeners.get(i).profilesAdded(e);
- }
- }
-
- /**
- * Creates and fires a "profilesRemoved" event for the given profile
result.
- */
- private void fireProfileRemoved(TableProfileResult victim) {
- if (victim == null) throw new NullPointerException("Can't fire
event for removing null profile");
- ProfileChangeEvent e = new ProfileChangeEvent(this, victim);
- for (int i = profileChangeListeners.size() - 1; i >= 0; i--) {
- profileChangeListeners.get(i).profilesRemoved(e);
}
}
Modified:
trunk/src/ca/sqlpower/architect/profile/event/ProfileChangeEvent.java
==============================================================================
--- trunk/src/ca/sqlpower/architect/profile/event/ProfileChangeEvent.java
(original)
+++ trunk/src/ca/sqlpower/architect/profile/event/ProfileChangeEvent.java
Thu Oct 16 15:45:43 2008
@@ -41,17 +41,6 @@
final List<ProfileResult> profileResultList;
/**
- * Creates a profile change event for the given single profile object.
- *
- * @param source The profile manager that gained or lost a profile.
- * @param pr The profile gained or lost.
- */
- public ProfileChangeEvent(ProfileManager source, ProfileResult pr) {
- super(source);
- this.profileResultList = Collections.singletonList(pr);
- }
-
- /**
* Creates a profile change event for the given list of profile
objects.
* The given list is copied, so it is safe to modify the list you pass
* in after creating this event.
Modified: trunk/src/ca/sqlpower/architect/swingui/dbtree/DBTreeModel.java
==============================================================================
--- trunk/src/ca/sqlpower/architect/swingui/dbtree/DBTreeModel.java
(original)
+++ trunk/src/ca/sqlpower/architect/swingui/dbtree/DBTreeModel.java Thu Oct
16 15:45:43 2008
@@ -42,9 +42,6 @@
import ca.sqlpower.architect.SQLObjectListener;
import ca.sqlpower.architect.SQLObjectRoot;
import ca.sqlpower.architect.SQLRelationship;
-import ca.sqlpower.architect.profile.ProfileResult;
-import ca.sqlpower.architect.profile.event.ProfileChangeEvent;
-import ca.sqlpower.architect.profile.event.ProfileChangeListener;
import ca.sqlpower.architect.swingui.ASUtils;
public class DBTreeModel implements TreeModel, SQLObjectListener,
java.io.Serializable {
@@ -77,40 +74,6 @@
this.root = root;
this.treeModelListeners = new LinkedList();
ArchitectUtils.listenToHierarchy(this, root);
-
- if (session != null) {
- session.getProfileManager().addProfileChangeListener(new
ProfileChangeListener(){
-
- public void profileListChanged(ProfileChangeEvent
event) {
- //This will not change the status of the profiles so ignore
it
- }
-
- /**
- * Note this will usually not be run from the event
thread
- */
-
- public void profilesAdded(ProfileChangeEvent e) {
- for (ProfileResult pr : e.getProfileResults()) {
- if (logger.isDebugEnabled()) logger.debug("Removing
profile "+pr); //$NON-NLS-1$
- SQLObjectEvent soe = new
SQLObjectEvent(pr.getProfiledObject(),"profile"); //$NON-NLS-1$
- processSQLObjectChanged(soe);
- }
- }
-
- public void profilesRemoved(ProfileChangeEvent e) {
- for (ProfileResult pr : e.getProfileResults()) {
- if (logger.isDebugEnabled()) logger.debug("Removing
profile "+pr); //$NON-NLS-1$
-
- // FIXME here's a crazy idea: if you want something to
be a bound property of
- // SQLTable, why not make it one?
- SQLObjectEvent soe = new
SQLObjectEvent(pr.getProfiledObject(),"profile"); //$NON-NLS-1$
-
- processSQLObjectChanged(soe);
- }
- }
-
- });
- }
}
public Object getRoot() {