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() {

Reply via email to