[201159] model rework
diff --git a/jpa/plugins/org.eclipse.jpt.utility/META-INF/MANIFEST.MF b/jpa/plugins/org.eclipse.jpt.utility/META-INF/MANIFEST.MF
index f43d949..2eedcd5 100644
--- a/jpa/plugins/org.eclipse.jpt.utility/META-INF/MANIFEST.MF
+++ b/jpa/plugins/org.eclipse.jpt.utility/META-INF/MANIFEST.MF
@@ -13,5 +13,6 @@
  org.eclipse.jpt.utility.internal.model.value,
  org.eclipse.jpt.utility.internal.model.value.prefs,
  org.eclipse.jpt.utility.internal.model.value.swing,
- org.eclipse.jpt.utility.internal.node
+ org.eclipse.jpt.utility.internal.node,
+ org.eclipse.jpt.utility.internal.swing
 Bundle-RequiredExecutionEnvironment: J2SE-1.5
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/SimpleStringMatcher.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/SimpleStringMatcher.java
new file mode 100644
index 0000000..122cd68
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/SimpleStringMatcher.java
@@ -0,0 +1,261 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal;
+
+import java.io.Serializable;
+import java.util.regex.Pattern;
+
+// TODO the regex code is not very fast - we could probably do better,
+// hand-coding the matching algorithm (eclipse StringMatcher?)
+/**
+ * This class implements a simple string-matching algorithm that is a little
+ * more user-friendly than standard regular expressions. Instantiate a
+ * string matcher with a filter pattern and then you can use the matcher
+ * to determine whether another string (or object) matches the pattern.
+ * You can also specify whether the matching should be case-sensitive.
+ * 
+ * The pattern can contain two "meta-characters":
+ * 	'*' will match any set of zero or more characters
+ * 	'?' will match any single character
+ * 
+ * Subclasses can override #prefix() and/or #suffix() to change what
+ * strings are prepended or appended to the original pattern string.
+ * This can offer a slight performance improvement over concatenating
+ * strings before calling #setPatternString(String).
+ * By default, a '*' is appended to every string.
+ * 
+ * This class also uses the string-matching algorithm to "filter" objects
+ * (and, as a result, also implements the Filter interface).
+ * A string converter is used to determine what string aspect of the
+ * object is compared to the pattern. By default the string returned
+ * by the object's #toString() method is passed to the pattern matcher.
+ */
+public class SimpleStringMatcher
+	implements StringMatcher, Filter, Serializable
+{
+
+	/** An adapter that converts the objects into strings to be matched with the pattern. */
+	private StringConverter stringConverter;
+
+	/** The string used to construct the regular expression pattern. */
+	private String patternString;
+
+	/** Whether the matcher ignores case - the default is true. */
+	private boolean ignoresCase;
+
+	/** The regular expression pattern built from the pattern string. */
+	private Pattern pattern;
+
+	/** A list of the meta-characters we need to escape if found in the pattern string. */
+	public static final char[] REG_EX_META_CHARS = { '(', '[', '{', '\\', '^', '$', '|', ')', '?', '*', '+', '.' };
+
+	private static final long serialVersionUID = 1L;
+
+
+	// ********** constructors **********
+
+	/**
+	 * Construct a string matcher with an pattern that will match
+	 * any string and ignore case.
+	 */
+	public SimpleStringMatcher() {
+		this("*");
+	}
+
+	/**
+	 * Construct a string matcher with the specified pattern
+	 * that will ignore case.
+	 */
+	public SimpleStringMatcher(String patternString) {
+		this(patternString, true);
+	}
+
+	/**
+	 * Construct a string matcher with the specified pattern that will
+	 * ignore case as specified.
+	 */
+	public SimpleStringMatcher(String patternString, boolean ignoresCase) {
+		super();
+		this.patternString = patternString;
+		this.ignoresCase = ignoresCase;
+		this.initialize();
+	}
+
+
+	// ********** initialization **********
+
+	protected void initialize() {
+		this.stringConverter = StringConverter.Default.instance();
+		this.rebuildPattern();
+	}
+
+	/**
+	 * Given the current pattern string and case-sensitivity setting,
+	 * re-build the regular expression pattern.
+	 */
+	protected synchronized void rebuildPattern() {
+		this.pattern = this.buildPattern();
+	}
+
+	/**
+	 * Given the current pattern string and case-sensitivity setting,
+	 * build and return a regular expression pattern that can be used
+	 * to match strings.
+	 */
+	protected Pattern buildPattern() {
+		int patternFlags = 0x0;
+		if (this.ignoresCase) {
+			patternFlags = Pattern.UNICODE_CASE | Pattern.CASE_INSENSITIVE;
+		}
+		return Pattern.compile(this.convertToRegEx(this.patternString), patternFlags);
+	}
+
+
+	// ********** StringMatcher implementation **********
+
+	/**
+	 * @see StringMatcher#setPatternString(String)
+	 */
+	public synchronized void setPatternString(String patternString) {
+		this.patternString = patternString;
+		this.rebuildPattern();
+	}
+
+	/**
+	 * Return whether the specified string matches the pattern.
+	 */
+	public synchronized boolean matches(String string) {
+		return this.pattern.matcher(string).matches();
+	}
+
+
+	// ********** Filter implementation **********
+
+	public synchronized boolean accept(Object o) {
+		return this.matches(this.stringConverter.convertToString(o));
+	}
+
+
+	// ********** accessors **********
+
+	/**
+	 * Return the string converter used to convert the objects
+	 * passed to the matcher into strings.
+	 */
+	public synchronized StringConverter getStringConverter() {
+		return this.stringConverter;
+	}
+
+	/**
+	 * Set the string converter used to convert the objects
+	 * passed to the matcher into strings.
+	 */
+	public synchronized void setStringConverter(StringConverter stringConverter) {
+		this.stringConverter = stringConverter;
+	}
+
+	/**
+	 * Return the original pattern string.
+	 */
+	public synchronized String getPatternString() {
+		return this.patternString;
+	}
+
+	/**
+	 * Return whether the matcher ignores case.
+	 */
+	public synchronized boolean ignoresCase() {
+		return this.ignoresCase;
+	}
+
+	/**
+	 * Set whether the matcher ignores case.
+	 */
+	public synchronized void setIgnoresCase(boolean ignoresCase) {
+		this.ignoresCase = ignoresCase;
+		this.rebuildPattern();
+	}
+
+	/**
+	 * Return the regular expression pattern.
+	 */
+	public synchronized Pattern getPattern() {
+		return this.pattern;
+	}
+
+
+	// ********** other public API **********
+
+	/**
+	 * Return the regular expression corresponding to
+	 * the original pattern string.
+	 */
+	public synchronized String regularExpression() {
+		return this.convertToRegEx(this.patternString);
+	}
+
+
+	// ********** converting **********
+
+	/**
+	 * Convert the specified string to a regular expression.
+	 */
+	protected String convertToRegEx(String string) {
+		StringBuffer sb = new StringBuffer(string.length() + 10);
+		this.convertToRegExOn(this.prefix(), sb);
+		this.convertToRegExOn(string, sb);
+		this.convertToRegExOn(this.suffix(), sb);
+		return sb.toString();
+	}
+
+	/**
+	 * Return any prefix that should be prepended to the original
+	 * string. By default, there is no prefix.
+	 */
+	protected String prefix() {
+		return "";
+	}
+
+	/**
+	 * Return any suffix that should be appended to the original
+	 * string. Since this class is typically used in UI situation where
+	 * the user is typing in a pattern used to filter a list, the default
+	 * suffix is a wildcard character.
+	 */
+	protected String suffix() {
+		return "*";
+	}
+
+	/**
+	 * Convert the specified string to a regular expression.
+	 */
+	protected void convertToRegExOn(String string, StringBuffer sb) {
+		char[] charArray = string.toCharArray();
+		int length = charArray.length;
+		for (int i = 0; i < length; i++) {
+			char c = charArray[i];
+			// convert user-friendly meta-chars into regex meta-chars
+			if (c == '*') {
+				sb.append(".*");
+				continue;
+			}
+			if (c == '?') {
+				sb.append('.');
+				continue;
+			}
+			// escape regex meta-chars
+			if (CollectionTools.contains(REG_EX_META_CHARS, c)) {
+				sb.append('\\');
+			}
+			sb.append(c);
+		}
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/StringMatcher.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/StringMatcher.java
new file mode 100644
index 0000000..5dd8f28
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/StringMatcher.java
@@ -0,0 +1,51 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal;
+
+/**
+ * This interface defines a simple API for allowing "pluggable"
+ * string matchers that can be configured with a pattern string
+ * then used to determine what strings match the pattern.
+ */
+public interface StringMatcher {
+
+	/**
+	 * Set the pattern string used to determine future
+	 * matches. The format and semantics of the pattern
+	 * string are determined by the contract between the
+	 * client and the server.
+	 */
+	void setPatternString(String patternString);
+
+	/**
+	 * Return whether the specified string matches the
+	 * established pattern string. The semantics of a match
+	 * is determined by the contract between the
+	 * client and the server.
+	 */
+	boolean matches(String string);
+
+
+	StringMatcher NULL_INSTANCE =
+		new StringMatcher() {
+			public void setPatternString(String patternString) {
+				// ignore the pattern string
+			}
+			public boolean matches(String string) {
+				// everything is a match
+				return true;
+			}
+			@Override
+			public String toString() {
+				return "NullStringMatcher";
+			}
+		};
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/AbstractTreeModel.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/AbstractTreeModel.java
new file mode 100644
index 0000000..2229fa8
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/AbstractTreeModel.java
@@ -0,0 +1,220 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.model.value.swing;
+
+import java.io.Serializable;
+
+import javax.swing.event.EventListenerList;
+import javax.swing.event.TreeModelEvent;
+import javax.swing.event.TreeModelListener;
+import javax.swing.tree.TreeModel;
+
+/**
+ * Abstract class that should have been provided by the JDK
+ * (à la javax.swing.AbstractListModel). This class provides:
+ * - support for a collection of listeners
+ * - a number of convenience methods for firing events for those listeners
+ */
+public abstract class AbstractTreeModel implements TreeModel, Serializable {
+
+	/** Our listeners. */
+	protected EventListenerList listenerList;
+
+
+	// ********** constructors/initialization **********
+
+	protected AbstractTreeModel() {
+		super();
+		this.initialize();
+	}
+
+	protected void initialize() {
+		this.listenerList = new EventListenerList();
+	}
+
+
+	// ********** partial TreeModel implementation **********
+
+	public void addTreeModelListener(TreeModelListener l) {
+		this.listenerList.add(TreeModelListener.class, l);
+	}
+
+	public void removeTreeModelListener(TreeModelListener l) {
+		this.listenerList.remove(TreeModelListener.class, l);
+	}
+
+
+	// ********** queries **********
+
+	/**
+	 * Return the model's current collection of listeners.
+	 * (There seems to be a pattern of making this type of method public;
+	 * although it should probably be protected....)
+	 */
+	public TreeModelListener[] getTreeModelListeners() {
+ 		return this.listenerList.getListeners(TreeModelListener.class);
+	}
+
+	/**
+	 * Return whether this model has no listeners.
+	 */
+	protected boolean hasNoTreeModelListeners() {
+		return this.listenerList.getListenerCount(TreeModelListener.class) == 0;
+	}
+
+	/**
+	 * Return whether this model has any listeners.
+	 */
+	protected boolean hasTreeModelListeners() {
+		return ! this.hasNoTreeModelListeners();
+	}
+
+
+	// ********** behavior **********
+
+	/**
+	 * Notify listeners of a model change.
+	 * A significant property of the nodes changed, but the nodes themselves
+	 * are still the same objects.
+	 * @see javax.swing.event.TreeModelEvent
+	 * @see javax.swing.event.TreeModelListener
+	 */
+	protected void fireTreeNodesChanged(Object[] path, int[] childIndices, Object[] children) {
+		// guaranteed to return a non-null array
+		Object[] listeners = this.listenerList.getListenerList();
+		TreeModelEvent e = null;
+		// process the listeners last to first, notifying
+		// those that are interested in this event
+		for (int i = listeners.length-2; i>=0; i-=2) {
+			if (listeners[i]==TreeModelListener.class) {
+				// lazily create the event
+				if (e == null) {
+					e = new TreeModelEvent(this, path, childIndices, children);
+				}
+				((TreeModelListener) listeners[i+1]).treeNodesChanged(e);
+			}
+		}
+	}
+
+
+	/**
+	 * Notify listeners of a model change.
+	 * A significant property of the node changed, but the node itself is the same object.
+	 * @see javax.swing.event.TreeModelEvent
+	 * @see javax.swing.event.TreeModelListener
+	 */
+	protected void fireTreeNodeChanged(Object[] path, int childIndex, Object child) {
+		this.fireTreeNodesChanged(path, new int[] {childIndex}, new Object[] {child});
+	}
+
+	/**
+	 * Notify listeners of a model change.
+	 * A significant property of the root changed, but the root itself is the same object.
+	 * @see javax.swing.event.TreeModelEvent
+	 * @see javax.swing.event.TreeModelListener
+	 */
+	protected void fireTreeRootChanged(Object root) {
+		this.fireTreeNodesChanged(new Object[] {root}, null, null);
+	}
+
+	/**
+	 * Notify listeners of a model change.
+	 * @see javax.swing.event.TreeModelEvent
+	 * @see javax.swing.event.TreeModelListener
+	 */
+	protected void fireTreeNodesInserted(Object[] path, int[] childIndices, Object[] children) {
+		// guaranteed to return a non-null array
+		Object[] listeners = this.listenerList.getListenerList();
+		TreeModelEvent e = null;
+		// process the listeners last to first, notifying
+		// those that are interested in this event
+		for (int i = listeners.length-2; i>=0; i-=2) {
+			if (listeners[i]==TreeModelListener.class) {
+				// lazily create the event
+				if (e == null) {
+					e = new TreeModelEvent(this, path, childIndices, children);
+				}
+				((TreeModelListener) listeners[i+1]).treeNodesInserted(e);
+			}
+		}
+	}
+
+	/**
+	 * Notify listeners of a model change.
+	 * @see javax.swing.event.TreeModelEvent
+	 * @see javax.swing.event.TreeModelListener
+	 */
+	protected void fireTreeNodeInserted(Object[] path, int childIndex, Object child) {
+		this.fireTreeNodesInserted(path, new int[] {childIndex}, new Object[] {child});
+	}
+
+	/**
+	 * Notify listeners of a model change.
+	 * @see javax.swing.event.TreeModelEvent
+	 * @see javax.swing.event.TreeModelListener
+	 */
+	protected void fireTreeNodesRemoved(Object[] path, int[] childIndices, Object[] children) {
+		// guaranteed to return a non-null array
+		Object[] listeners = this.listenerList.getListenerList();
+		TreeModelEvent e = null;
+		// process the listeners last to first, notifying
+		// those that are interested in this event
+		for (int i = listeners.length-2; i>=0; i-=2) {
+			if (listeners[i]==TreeModelListener.class) {
+				// lazily create the event
+				if (e == null) {
+					e = new TreeModelEvent(this, path, childIndices, children);
+				}
+				((TreeModelListener) listeners[i+1]).treeNodesRemoved(e);
+			}
+		}
+	}
+
+	/**
+	 * Notify listeners of a model change.
+	 * @see javax.swing.event.TreeModelEvent
+	 * @see javax.swing.event.TreeModelListener
+	 */
+	protected void fireTreeNodeRemoved(Object[] path, int childIndex, Object child) {
+		this.fireTreeNodesRemoved(path, new int[] {childIndex}, new Object[] {child});
+	}
+
+	/**
+	 * Notify listeners of a model change.
+	 * @see javax.swing.event.TreeModelEvent
+	 * @see javax.swing.event.TreeModelListener
+	 */
+	protected void fireTreeStructureChanged(Object[] path) {
+		// guaranteed to return a non-null array
+		Object[] listeners = this.listenerList.getListenerList();
+		TreeModelEvent e = null;
+		// process the listeners last to first, notifying
+		// those that are interested in this event
+		for (int i = listeners.length-2; i>=0; i-=2) {
+			if (listeners[i]==TreeModelListener.class) {
+				// lazily create the event
+				if (e == null) {
+					e = new TreeModelEvent(this, path);
+				}
+				((TreeModelListener) listeners[i+1]).treeStructureChanged(e);
+			}
+		}
+	}
+
+	/**
+	 * Notify listeners of a model change.
+	 * @see javax.swing.event.TreeModelEvent
+	 * @see javax.swing.event.TreeModelListener
+	 */
+	protected void fireTreeRootReplaced(Object newRoot) {
+		this.fireTreeStructureChanged(new Object[] {newRoot});
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/CheckBoxModelAdapter.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/CheckBoxModelAdapter.java
new file mode 100644
index 0000000..7978ec7
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/CheckBoxModelAdapter.java
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.model.value.swing;
+
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+
+/**
+ * This javax.swing.ButtonModel can be used to keep a listener
+ * (e.g. a JCheckBox) in synch with a PropertyValueModel that
+ * holds a boolean.
+ * 
+ * Maybe not the richest class in our toolbox, but it was the
+ * victim of refactoring....  ~bjv
+ */
+public class CheckBoxModelAdapter extends ToggleButtonModelAdapter {
+
+
+	// ********** constructors **********
+
+	/**
+	 * Constructor - the boolean holder is required.
+	 */
+	public CheckBoxModelAdapter(PropertyValueModel booleanHolder, boolean defaultValue) {
+		super(booleanHolder, defaultValue);
+	}
+
+	/**
+	 * Constructor - the boolean holder is required.
+	 * The default value will be false.
+	 */
+	public CheckBoxModelAdapter(PropertyValueModel booleanHolder) {
+		super(booleanHolder);
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/ColumnAdapter.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/ColumnAdapter.java
new file mode 100644
index 0000000..62bcde0
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/ColumnAdapter.java
@@ -0,0 +1,49 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.model.value.swing;
+
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+
+
+/**
+ * This adapter is used by the table model adapter to
+ * convert a model object into the models used for each of
+ * the cells for the object's corresponding row in the table.
+ */
+public interface ColumnAdapter {
+	/**
+	 * Return the number of columns in the table.
+	 * Typically this is static.
+	 */
+	int getColumnCount();
+
+	/**
+	 * Return the name of the specified column.
+	 */
+	String getColumnName(int index);
+
+	/**
+	 * Return the class of the specified column.
+	 */
+	Class<?> getColumnClass(int index);
+
+	/**
+	 * Return whether the specified column is editable.
+	 * Typically this is the same for every row.
+	 */
+	boolean isColumnEditable(int index);
+
+	/**
+	 * Return the cell models for the specified subject
+	 * that corresponds to a single row in the table.
+	 */
+	PropertyValueModel[] cellModels(Object subject);
+
+}
\ No newline at end of file
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/ComboBoxModelAdapter.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/ComboBoxModelAdapter.java
new file mode 100644
index 0000000..11187ae
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/ComboBoxModelAdapter.java
@@ -0,0 +1,141 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.model.value.swing;
+
+import javax.swing.ComboBoxModel;
+
+import org.eclipse.jpt.utility.internal.StringTools;
+import org.eclipse.jpt.utility.internal.model.event.PropertyChangeEvent;
+import org.eclipse.jpt.utility.internal.model.listener.PropertyChangeListener;
+import org.eclipse.jpt.utility.internal.model.value.CollectionValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ListValueModel;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+
+/**
+ * This javax.swing.ComboBoxModel can be used to keep a ListDataListener
+ * (e.g. a JComboBox) in synch with a ListValueModel (or a CollectionValueModel).
+ * For combo boxes, the model object that holds the current selection is
+ * typically a different model object than the one that holds the collection
+ * of choices.
+ * 
+ * For example, a MWReference (the selectionOwner) has an attribute
+ * "sourceTable" (the collectionOwner)
+ * which holds on to a collection of MWDatabaseFields. When the selection
+ * is changed this model will keep the listeners aware of the changes.
+ * The inherited list model will keep its listeners aware of changes to the
+ * collection model
+ * 
+ * In addition to the collection holder required by the superclass,
+ * an instance of this ComboBoxModel must be supplied with a
+ * selection holder, which is a PropertyValueModel that provides access
+ * to the selection (typically a PropertyAspectAdapter).
+ */
+public class ComboBoxModelAdapter extends ListModelAdapter implements ComboBoxModel {
+
+	protected PropertyValueModel selectionHolder;
+	protected PropertyChangeListener selectionListener;
+
+
+	// ********** constructors **********
+
+	/**
+	 * Constructor - the list holder and selection holder are required;
+	 */
+	public ComboBoxModelAdapter(ListValueModel listHolder, PropertyValueModel selectionHolder) {
+		super(listHolder);
+		if (selectionHolder == null) {
+			throw new NullPointerException();
+		}
+		this.selectionHolder = selectionHolder;
+	}
+
+	/**
+	 * Constructor - the collection holder and selection holder are required;
+	 */
+	public ComboBoxModelAdapter(CollectionValueModel collectionHolder, PropertyValueModel selectionHolder) {
+		super(collectionHolder);
+		if (selectionHolder == null) {
+			throw new NullPointerException();
+		}
+		this.selectionHolder = selectionHolder;
+	}
+
+
+	// ********** initialization **********
+
+	/**
+	 * Extend to build the selection listener.
+	 */
+	@Override
+	protected void initialize() {
+		super.initialize();
+		this.selectionListener = this.buildSelectionListener();
+	}
+
+	protected PropertyChangeListener buildSelectionListener() {
+		return new PropertyChangeListener() {
+			public void propertyChanged(PropertyChangeEvent e) {
+				// notify listeners that the selection has changed
+				ComboBoxModelAdapter.this.fireSelectionChanged();
+			}
+			@Override
+			public String toString() {
+				return "selection listener";
+			}
+		};
+	}
+
+
+	// ********** ComboBoxModel implementation **********
+
+	public Object getSelectedItem() {
+		return this.selectionHolder.getValue();
+	}
+
+	public void setSelectedItem(Object selectedItem) {
+		this.selectionHolder.setValue(selectedItem);
+	}
+
+
+	// ********** behavior **********
+
+	/**
+	 * Extend to engage the selection holder.
+	 */
+	@Override
+	protected void engageModel() {
+		super.engageModel();
+		this.selectionHolder.addPropertyChangeListener(ValueModel.VALUE, this.selectionListener);
+	}
+
+	/**
+	 * Extend to disengage the selection holder.
+	 */
+	@Override
+	protected void disengageModel() {
+		this.selectionHolder.removePropertyChangeListener(ValueModel.VALUE, this.selectionListener);
+		super.disengageModel();
+	}
+
+	/**
+	 * Notify the listeners that the selection has changed.
+	 */
+	protected void fireSelectionChanged() {
+		// I guess this will work...
+		this.fireContentsChanged(this, -1, -1);
+	}
+
+	@Override
+	public String toString() {
+		return StringTools.buildToStringFor(this, this.selectionHolder + ":" + this.listHolder);
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/DateSpinnerModelAdapter.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/DateSpinnerModelAdapter.java
new file mode 100644
index 0000000..308b748
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/DateSpinnerModelAdapter.java
@@ -0,0 +1,192 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.model.value.swing;
+
+import java.util.Calendar;
+import java.util.Date;
+
+import javax.swing.SpinnerDateModel;
+import javax.swing.event.ChangeListener;
+
+import org.eclipse.jpt.utility.internal.StringTools;
+import org.eclipse.jpt.utility.internal.model.event.PropertyChangeEvent;
+import org.eclipse.jpt.utility.internal.model.listener.PropertyChangeListener;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+
+/**
+ * This javax.swing.SpinnerDateModel can be used to keep a ChangeListener
+ * (e.g. a JSpinner) in synch with a PropertyValueModel that holds a date.
+ * 
+ * This class must be a sub-class of SpinnerDateModel because of some
+ * crappy jdk code....  ~bjv
+ * @see javax.swing.JSpinner#createEditor(javax.swing.SpinnerModel)
+ * 
+ * If this class needs to be modified, it would behoove us to review the
+ * other, similar classes:
+ * @see ListSpinnerModelAdapter
+ * @see NumberSpinnerModelAdapter
+ */
+public class DateSpinnerModelAdapter extends SpinnerDateModel {
+
+	/**
+	 * The default spinner value; used when the underlying model date value is null.
+	 * The default is the current date.
+	 */
+	private Date defaultValue;
+
+	/** A value model on the underlying date. */
+	private PropertyValueModel dateHolder;
+
+	/** A listener that allows us to synchronize with changes made to the underlying date. */
+	private PropertyChangeListener dateChangeListener;
+
+
+	// ********** constructors **********
+
+	/**
+	 * Constructor - the date holder is required.
+	 * The default spinner value is the current date.
+	 */
+	public DateSpinnerModelAdapter(PropertyValueModel dateHolder) {
+		this(dateHolder, new Date());
+	}
+
+	/**
+	 * Constructor - the date holder and default value are required.
+	 */
+	public DateSpinnerModelAdapter(PropertyValueModel dateHolder, Date defaultValue) {
+		this(dateHolder, null, null, Calendar.DAY_OF_MONTH, defaultValue);
+	}
+
+	/**
+	 * Constructor - the date holder is required.
+	 * The default spinner value is the current date.
+	 */
+	public DateSpinnerModelAdapter(PropertyValueModel dateHolder, Comparable start, Comparable end, int calendarField) {
+		this(dateHolder, start, end, calendarField, new Date());
+	}
+
+	/**
+	 * Constructor - the date holder is required.
+	 */
+	public DateSpinnerModelAdapter(PropertyValueModel dateHolder, Comparable start, Comparable end, int calendarField, Date defaultValue) {
+		super(dateHolder.getValue() == null ? defaultValue : (Date) dateHolder.getValue(), start, end, calendarField);
+		this.dateHolder = dateHolder;
+		this.dateChangeListener = this.buildDateChangeListener();
+		// postpone listening to the underlying date
+		// until we have listeners ourselves...
+		this.defaultValue = defaultValue;
+	}
+
+
+	// ********** initialization **********
+
+	private PropertyChangeListener buildDateChangeListener() {
+		return new PropertyChangeListener() {
+			public void propertyChanged(PropertyChangeEvent e) {
+				DateSpinnerModelAdapter.this.synchronize(e.newValue());
+			}
+			@Override
+			public String toString() {
+				return "date listener";
+			}
+		};
+	}
+
+
+	// ********** SpinnerModel implementation **********
+
+	/**
+	 * Extend to check whether this method is being called before we 
+	 * have any listeners.
+	 * This is necessary because some crappy jdk code gets the value
+	 * from the model *before* listening to the model.  ~bjv
+	 * @see javax.swing.JSpinner.DefaultEditor(javax.swing.JSpinner)
+	 */
+    @Override
+	public Object getValue() {
+		if (this.getChangeListeners().length == 0) {
+			// sorry about this "lateral" call to super  ~bjv
+			super.setValue(this.spinnerValueOf(this.dateHolder.getValue()));
+		}
+		return super.getValue();
+	}
+
+	/**
+	 * Extend to update the underlying date directly.
+	 * The resulting event will be ignored: @see #synchronize(Object).
+	 */
+	@Override
+	public void setValue(Object value) {
+		super.setValue(value);
+		this.dateHolder.setValue(value);
+	}
+
+	/**
+	 * Extend to start listening to the underlying date if necessary.
+	 */
+	@Override
+	public void addChangeListener(ChangeListener listener) {
+		if (this.getChangeListeners().length == 0) {
+			this.dateHolder.addPropertyChangeListener(ValueModel.VALUE, this.dateChangeListener);
+			this.synchronize(this.dateHolder.getValue());
+		}
+		super.addChangeListener(listener);
+	}
+
+	/**
+	 * Extend to stop listening to the underlying date if appropriate.
+	 */
+    @Override
+	public void removeChangeListener(ChangeListener listener) {
+		super.removeChangeListener(listener);
+		if (this.getChangeListeners().length == 0) {
+			this.dateHolder.removePropertyChangeListener(ValueModel.VALUE, this.dateChangeListener);
+		}
+	}
+
+
+	// ********** queries **********
+
+	protected Date getDefaultValue() {
+		return this.defaultValue;
+	}
+
+	/**
+	 * Convert to a non-null value.
+	 */
+	protected Object spinnerValueOf(Object value) {
+		return (value == null) ? this.getDefaultValue() : value;
+	}
+
+
+	// ********** behavior **********
+
+	/**
+	 * Set the spinner value if it has changed.
+	 */
+	void synchronize(Object value) {
+		Object newValue = this.spinnerValueOf(value);
+		// check to see whether the spinner date has already been synchronized
+		// (via #setValue())
+		if ( ! this.getValue().equals(newValue)) {
+			this.setValue(newValue);
+		}
+	}
+
+
+	// ********** standard methods **********
+
+	public String toString() {
+		return StringTools.buildToStringFor(this, this.dateHolder);
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/DocumentAdapter.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/DocumentAdapter.java
new file mode 100644
index 0000000..394da65
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/DocumentAdapter.java
@@ -0,0 +1,368 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.model.value.swing;
+
+import java.io.Serializable;
+import java.util.EventObject;
+
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.event.EventListenerList;
+import javax.swing.event.UndoableEditEvent;
+import javax.swing.event.UndoableEditListener;
+import javax.swing.text.AttributeSet;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Document;
+import javax.swing.text.Element;
+import javax.swing.text.PlainDocument;
+import javax.swing.text.Position;
+import javax.swing.text.Segment;
+
+import org.eclipse.jpt.utility.internal.StringTools;
+import org.eclipse.jpt.utility.internal.model.event.PropertyChangeEvent;
+import org.eclipse.jpt.utility.internal.model.listener.PropertyChangeListener;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+
+/**
+ * This javax.swing.text.Document can be used to keep a DocumentListener
+ * (e.g. a JTextField) in synch with a PropertyValueModel that holds a string.
+ * 
+ * NB: This model should only be used for "small" documents;
+ * i.e. documents used by text fields, not text panes.
+ * @see #synchronizeDelegate(String)
+ */
+public class DocumentAdapter implements Document, Serializable {
+
+	/** The delegate document whose behavior we "enhance". */
+	protected Document delegate;
+
+	/** A listener that allows us to forward any changes made to the delegate document. */
+	protected CombinedListener delegateListener;
+
+	/** A value model on the underlying model string. */
+	protected PropertyValueModel stringHolder;
+
+	/** A listener that allows us to synchronize with changes made to the underlying model string. */
+	protected PropertyChangeListener stringListener;
+
+    /** The event listener list for the document. */
+    protected EventListenerList listenerList = new EventListenerList();
+
+
+	// ********** constructors **********
+
+	/**
+	 * Default constructor - initialize stuff.
+	 */
+	private DocumentAdapter() {
+		super();
+		this.initialize();
+	}
+
+	/**
+	 * Constructor - the string holder is required.
+	 * Wrap the specified document.
+	 */
+	public DocumentAdapter(PropertyValueModel stringHolder, Document delegate) {
+		this();
+		if (stringHolder == null || delegate == null) {
+			throw new NullPointerException();
+		}
+		this.stringHolder = stringHolder;
+		// postpone listening to the underlying model string
+		// until we have listeners ourselves...
+		this.delegate = delegate;
+	}
+
+	/**
+	 * Constructor - the string holder is required.
+	 * Wrap a plain document.
+	 */
+	public DocumentAdapter(PropertyValueModel stringHolder) {
+		this(stringHolder, new PlainDocument());
+	}
+
+
+	// ********** initialization **********
+
+	protected void initialize() {
+		this.stringListener = this.buildStringListener();
+		this.delegateListener = this.buildDelegateListener();
+	}
+
+	protected PropertyChangeListener buildStringListener() {
+		return new PropertyChangeListener() {
+			public void propertyChanged(PropertyChangeEvent e) {
+				DocumentAdapter.this.stringChanged(e);
+			}
+			@Override
+			public String toString() {
+				return "string listener";
+			}
+		};
+	}
+
+	protected CombinedListener buildDelegateListener() {
+		return new InternalListener();
+	}
+
+
+	// ********** Document implementation **********
+
+	public int getLength() {
+		return this.delegate.getLength();
+	}
+
+	/**
+	 * Extend to start listening to the underlying models if necessary.
+	 */
+	public void addDocumentListener(DocumentListener listener) {
+		if (this.listenerList.getListenerCount(DocumentListener.class) == 0) {
+			this.delegate.addDocumentListener(this.delegateListener);
+			this.engageStringHolder();
+		}
+		this.listenerList.add(DocumentListener.class, listener);
+	}
+
+	/**
+	 * Extend to stop listening to the underlying models if appropriate.
+	 */
+	public void removeDocumentListener(DocumentListener listener) {
+		this.listenerList.remove(DocumentListener.class, listener);
+		if (this.listenerList.getListenerCount(DocumentListener.class) == 0) {
+			this.disengageStringHolder();
+			this.delegate.removeDocumentListener(this.delegateListener);
+		}
+	}
+
+	/**
+	 * Extend to start listening to the delegate document if necessary.
+	 */
+	public void addUndoableEditListener(UndoableEditListener listener) {
+		if (this.listenerList.getListenerCount(UndoableEditListener.class) == 0) {
+			this.delegate.addUndoableEditListener(this.delegateListener);
+		}
+		this.listenerList.add(UndoableEditListener.class, listener);
+	}
+
+	/**
+	 * Extend to stop listening to the delegate document if appropriate.
+	 */
+	public void removeUndoableEditListener(UndoableEditListener listener) {
+		this.listenerList.remove(UndoableEditListener.class, listener);
+		if (this.listenerList.getListenerCount(UndoableEditListener.class) == 0) {
+			this.delegate.removeUndoableEditListener(this.delegateListener);
+		}
+	}
+
+	public Object getProperty(Object key) {
+		return this.delegate.getProperty(key);
+	}
+
+	public void putProperty(Object key, Object value) {
+		this.delegate.putProperty(key, value);
+	}
+
+	/**
+	 * Extend to update the underlying model string directly.
+	 * The resulting event will be ignored: @see #synchronizeDelegate(String).
+	 */
+	public void remove(int offset, int len) throws BadLocationException {
+		this.delegate.remove(offset, len);
+		this.stringHolder.setValue(this.delegate.getText(0, this.delegate.getLength()));
+	}
+
+	/**
+	 * Extend to update the underlying model string directly.
+	 * The resulting event will be ignored: @see #synchronizeDelegate(String).
+	 */
+	public void insertString(int offset, String insertedString, AttributeSet a) throws BadLocationException {
+		this.delegate.insertString(offset, insertedString, a);
+		this.stringHolder.setValue(this.delegate.getText(0, this.delegate.getLength()));
+	}
+
+	public String getText(int offset, int length) throws BadLocationException {
+		return this.delegate.getText(offset, length);
+	}
+
+	public void getText(int offset, int length, Segment txt) throws BadLocationException {
+		this.delegate.getText(offset, length, txt);
+	}
+
+	public Position getStartPosition() {
+		return this.delegate.getStartPosition();
+	}
+
+	public Position getEndPosition() {
+		return this.delegate.getEndPosition();
+	}
+
+	public Position createPosition(int offs) throws BadLocationException {
+		return this.delegate.createPosition(offs);
+	}
+
+	public Element[] getRootElements() {
+		return this.delegate.getRootElements();
+	}
+
+	public Element getDefaultRootElement() {
+		return this.delegate.getDefaultRootElement();
+	}
+
+	public void render(Runnable r) {
+		this.delegate.render(r);
+	}
+
+
+	// ********** queries **********
+
+	public DocumentListener[] getDocumentListeners() {
+		return this.listenerList.getListeners(DocumentListener.class);
+	}
+
+	public UndoableEditListener[] getUndoableEditListeners() {
+		return this.listenerList.getListeners(UndoableEditListener.class);
+	}
+
+
+	// ********** behavior **********
+
+	/**
+	 * A third party has modified the underlying model string.
+	 * Synchronize the delegate document accordingly.
+	 */
+	protected void stringChanged(PropertyChangeEvent e) {
+		this.synchronizeDelegate((String) e.newValue());
+	}
+
+	/**
+	 * Replace the document's entire text string with the new string.
+	 */
+	protected void synchronizeDelegate(String s) {
+		try {
+			int len = this.delegate.getLength();
+			// check to see whether the delegate has already been synchronized
+			// (via #insertString() or #remove())
+			if ( ! this.delegate.getText(0, len).equals(s)) {
+				this.delegate.remove(0, len);
+				this.delegate.insertString(0, s, null);
+			}
+		} catch (BadLocationException ex) {
+			throw new IllegalStateException(ex.getMessage());	// this should not happen...
+		}
+	}
+
+	protected void engageStringHolder() {
+		this.stringHolder.addPropertyChangeListener(ValueModel.VALUE, this.stringListener);
+		this.synchronizeDelegate((String) this.stringHolder.getValue());
+	}
+
+	protected void disengageStringHolder() {
+		this.stringHolder.removePropertyChangeListener(ValueModel.VALUE, this.stringListener);
+	}
+
+	protected void delegateChangedUpdate(DocumentEvent e) {
+		// no need to lazy-initialize the event;
+		// we wouldn't get here if we did not have listeners...
+		DocumentEvent ee = new InternalDocumentEvent(this, e);
+		DocumentListener[] listeners = this.getDocumentListeners();
+		for (int i = listeners.length; i-- > 0; ) {
+			listeners[i].changedUpdate(ee);
+		}
+	}
+
+	protected void delegateInsertUpdate(DocumentEvent e) {
+		// no need to lazy-initialize the event;
+		// we wouldn't get here if we did not have listeners...
+		DocumentEvent ee = new InternalDocumentEvent(this, e);
+		DocumentListener[] listeners = this.getDocumentListeners();
+		for (int i = listeners.length; i-- > 0; ) {
+			listeners[i].insertUpdate(ee);
+		}
+	}
+
+	protected void delegateRemoveUpdate(DocumentEvent e) {
+		// no need to lazy-initialize the event;
+		// we wouldn't get here if we did not have listeners...
+		DocumentEvent ee = new InternalDocumentEvent(this, e);
+		DocumentListener[] listeners = this.getDocumentListeners();
+		for (int i = listeners.length; i-- > 0; ) {
+			listeners[i].removeUpdate(ee);
+		}
+	}
+
+	protected void delegateUndoableEditHappened(UndoableEditEvent e) {
+		// no need to lazy-initialize the event;
+		// we wouldn't get here if we did not have listeners...
+		UndoableEditEvent ee = new UndoableEditEvent(this, e.getEdit());
+		UndoableEditListener[] listeners = this.getUndoableEditListeners();
+		for (int i = listeners.length; i-- > 0; ) {
+			listeners[i].undoableEditHappened(ee);
+		}
+	}
+
+	// ********** standard methods **********
+
+	@Override
+	public String toString() {
+		return StringTools.buildToStringFor(this, this.stringHolder);
+	}
+
+
+// ********** inner class **********
+
+	protected interface CombinedListener extends DocumentListener, UndoableEditListener {
+		// just consolidate the two interfaces
+	}
+
+	protected class InternalListener implements CombinedListener {
+		public void changedUpdate(DocumentEvent e) {
+			DocumentAdapter.this.delegateChangedUpdate(e);
+		}
+		public void insertUpdate(DocumentEvent e) {
+			DocumentAdapter.this.delegateInsertUpdate(e);
+		}
+		public void removeUpdate(DocumentEvent e) {
+			DocumentAdapter.this.delegateRemoveUpdate(e);
+		}
+		public void undoableEditHappened(UndoableEditEvent e) {
+			DocumentAdapter.this.delegateUndoableEditHappened(e);
+		}
+	}
+	
+	protected static class InternalDocumentEvent
+		extends EventObject
+		implements DocumentEvent
+	{
+		protected DocumentEvent delegate;
+	
+		protected InternalDocumentEvent(Document document, DocumentEvent delegate) {
+			super(document);
+			this.delegate = delegate;
+		}
+		public ElementChange getChange(Element elem) {
+			return this.delegate.getChange(elem);
+		}
+		public Document getDocument() {
+			return (Document) this.source;
+		}
+		public int getLength() {
+			return this.delegate.getLength();
+		}
+		public int getOffset() {
+			return this.delegate.getOffset();
+		}
+		public EventType getType() {
+			return this.delegate.getType();
+		}
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/ListSpinnerModelAdapter.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/ListSpinnerModelAdapter.java
new file mode 100644
index 0000000..2e1559c
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/ListSpinnerModelAdapter.java
@@ -0,0 +1,213 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.model.value.swing;
+
+import java.util.Arrays;
+import java.util.List;
+
+import javax.swing.SpinnerListModel;
+import javax.swing.event.ChangeListener;
+
+import org.eclipse.jpt.utility.internal.StringTools;
+import org.eclipse.jpt.utility.internal.model.event.PropertyChangeEvent;
+import org.eclipse.jpt.utility.internal.model.listener.PropertyChangeListener;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+
+/**
+ * This javax.swing.SpinnerListModel can be used to keep a ChangeListener
+ * (e.g. a JSpinner) in synch with a PropertyValueModel that holds a value
+ * in the list.
+ * 
+ * This class must be a sub-class of SpinnerListModel because of some
+ * crappy jdk code....  ~bjv
+ * @see javax.swing.JSpinner#createEditor(javax.swing.SpinnerModel)
+ * 
+ * NB: This model should only be used for values that have a reasonably
+ * inexpensive #equals() implementation.
+ * @see #synchronize(Object)
+ * 
+ * If this class needs to be modified, it would behoove us to review the
+ * other, similar classes:
+ * @see DateSpinnerModelAdapter
+ * @see NumberSpinnerModelAdapter
+ */
+public class ListSpinnerModelAdapter extends SpinnerListModel {
+
+	/**
+	 * The default spinner value; used when the underlying model value is null.
+	 * The default is the first item on the list.
+	 */
+	private Object defaultValue;
+
+	/** A value model on the underlying value. */
+	private PropertyValueModel valueHolder;
+
+	/** A listener that allows us to synchronize with changes made to the underlying value. */
+	private PropertyChangeListener valueChangeListener;
+
+
+	// ********** constructors **********
+
+	/**
+	 * Constructor - the value holder is required.
+	 * Use the model value itself as the default spinner value.
+	 */
+	public ListSpinnerModelAdapter(PropertyValueModel valueHolder) {
+		this(valueHolder, valueHolder.getValue());
+	}
+
+	/**
+	 * Constructor - the value holder is required.
+	 */
+	public ListSpinnerModelAdapter(PropertyValueModel valueHolder, Object defaultValue) {
+		this(valueHolder, new Object[] {defaultValue}, defaultValue);
+	}
+
+	/**
+	 * Constructor - the value holder is required.
+	 * Use the first item in the list of values as the default spinner value.
+	 */
+	public ListSpinnerModelAdapter(PropertyValueModel valueHolder, Object[] values) {
+		this(valueHolder, values, values[0]);
+	}
+
+	/**
+	 * Constructor - the value holder is required.
+	 */
+	public ListSpinnerModelAdapter(PropertyValueModel valueHolder, Object[] values, Object defaultValue) {
+		this(valueHolder, Arrays.asList(values), defaultValue);
+	}
+
+	/**
+	 * Constructor - the value holder is required.
+	 * Use the first item in the list of values as the default spinner value.
+	 */
+	public ListSpinnerModelAdapter(PropertyValueModel valueHolder, List values) {
+		this(valueHolder, values, values.get(0));
+	}
+
+	/**
+	 * Constructor - the value holder is required.
+	 */
+	public ListSpinnerModelAdapter(PropertyValueModel valueHolder, List values, Object defaultValue) {
+		super(values);
+		this.valueHolder = valueHolder;
+		this.valueChangeListener = this.buildValueChangeListener();
+		// postpone listening to the underlying value
+		// until we have listeners ourselves...
+		this.defaultValue = defaultValue;
+	}
+
+
+	// ********** initialization **********
+
+	private PropertyChangeListener buildValueChangeListener() {
+		return new PropertyChangeListener() {
+			public void propertyChanged(PropertyChangeEvent e) {
+				ListSpinnerModelAdapter.this.synchronize(e.newValue());
+			}
+			@Override
+			public String toString() {
+				return "value listener";
+			}
+		};
+	}
+
+
+	// ********** SpinnerModel implementation **********
+
+	/**
+	 * Extend to check whether this method is being called before we 
+	 * have any listeners.
+	 * This is necessary because some crappy jdk code gets the value
+	 * from the model *before* listening to the model.  ~bjv
+	 * @see javax.swing.JSpinner.DefaultEditor(javax.swing.JSpinner)
+	 */
+    @Override
+	public Object getValue() {
+		if (this.getChangeListeners().length == 0) {
+			// sorry about this "lateral" call to super  ~bjv
+			super.setValue(this.spinnerValueOf(this.valueHolder.getValue()));
+		}
+		return super.getValue();
+	}
+
+	/**
+	 * Extend to update the underlying value directly.
+	 * The resulting event will be ignored: @see #synchronize(Object).
+	 */
+    @Override
+	public void setValue(Object value) {
+		super.setValue(value);
+		this.valueHolder.setValue(value);
+	}
+
+	/**
+	 * Extend to start listening to the underlying value if necessary.
+	 */
+    @Override
+	public void addChangeListener(ChangeListener listener) {
+		if (this.getChangeListeners().length == 0) {
+			this.valueHolder.addPropertyChangeListener(ValueModel.VALUE, this.valueChangeListener);
+			this.synchronize(this.valueHolder.getValue());
+		}
+		super.addChangeListener(listener);
+	}
+
+	/**
+	 * Extend to stop listening to the underlying value if appropriate.
+	 */
+    @Override
+	public void removeChangeListener(ChangeListener listener) {
+		super.removeChangeListener(listener);
+		if (this.getChangeListeners().length == 0) {
+			this.valueHolder.removePropertyChangeListener(ValueModel.VALUE, this.valueChangeListener);
+		}
+	}
+
+
+	// ********** queries **********
+
+	protected Object getDefaultValue() {
+		return this.defaultValue;
+	}
+
+	/**
+	 * Convert to a non-null value.
+	 */
+	protected Object spinnerValueOf(Object value) {
+		return (value == null) ? this.getDefaultValue() : value;
+	}
+
+
+	// ********** behavior **********
+
+	/**
+	 * Set the spinner value if it has changed.
+	 */
+	void synchronize(Object value) {
+		Object newValue = this.spinnerValueOf(value);
+		// check to see whether the spinner value has already been synchronized
+		// (via #setValue())
+		if ( ! this.getValue().equals(newValue)) {
+			this.setValue(newValue);
+		}
+	}
+
+
+	// ********** standard methods **********
+
+	@Override
+	public String toString() {
+		return StringTools.buildToStringFor(this, this.valueHolder);
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/NumberSpinnerModelAdapter.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/NumberSpinnerModelAdapter.java
new file mode 100644
index 0000000..4173425
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/NumberSpinnerModelAdapter.java
@@ -0,0 +1,217 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.model.value.swing;
+
+import javax.swing.SpinnerNumberModel;
+import javax.swing.event.ChangeListener;
+
+import org.eclipse.jpt.utility.internal.StringTools;
+import org.eclipse.jpt.utility.internal.model.event.PropertyChangeEvent;
+import org.eclipse.jpt.utility.internal.model.listener.PropertyChangeListener;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+
+/**
+ * This javax.swing.SpinnerNumberModel can be used to keep a ChangeListener
+ * (e.g. a JSpinner) in synch with a PropertyValueModel that holds a number.
+ * 
+ * This class must be a sub-class of SpinnerNumberModel because of some
+ * crappy jdk code....  ~bjv
+ * @see javax.swing.JSpinner#createEditor(javax.swing.SpinnerModel)
+ * 
+ * If this class needs to be modified, it would behoove us to review the
+ * other, similar classes:
+ * @see DateSpinnerModelAdapter
+ * @see ListSpinnerModelAdapter
+ */
+public class NumberSpinnerModelAdapter extends SpinnerNumberModel {
+
+	/**
+	 * The default spinner value; used when the
+	 * underlying model number value is null.
+	 */
+	private Number defaultValue;
+
+	/** A value model on the underlying number. */
+	private PropertyValueModel numberHolder;
+
+	/**
+	 * A listener that allows us to synchronize with
+	 * changes made to the underlying number.
+	 */
+	private PropertyChangeListener numberChangeListener;
+
+
+	// ********** constructors **********
+
+	/**
+	 * Constructor - the number holder is required.
+	 * The default spinner value is zero.
+	 * The step size is one.
+	 */
+	public NumberSpinnerModelAdapter(PropertyValueModel numberHolder) {
+		this(numberHolder, 0);
+	}
+
+	/**
+	 * Constructor - the number holder is required.
+	 * The step size is one.
+	 */
+	public NumberSpinnerModelAdapter(PropertyValueModel numberHolder, int defaultValue) {
+		this(numberHolder, null, null, new Integer(1), new Integer(defaultValue));
+	}
+
+	/**
+	 * Constructor - the number holder is required.
+	 * Use the minimum value as the default spinner value.
+	 */
+	public NumberSpinnerModelAdapter(PropertyValueModel numberHolder, int minimum, int maximum, int stepSize) {
+		this(numberHolder, minimum, maximum, stepSize, minimum);
+	}
+
+	/**
+	 * Constructor - the number holder is required.
+	 */
+	public NumberSpinnerModelAdapter(PropertyValueModel numberHolder, int minimum, int maximum, int stepSize, int defaultValue) {
+		this(numberHolder, new Integer(minimum), new Integer(maximum), new Integer(stepSize), new Integer(defaultValue));
+	}
+
+	/**
+	 * Constructor - the number holder is required.
+	 * Use the minimum value as the default spinner value.
+	 */
+	public NumberSpinnerModelAdapter(PropertyValueModel numberHolder, double value, double minimum, double maximum, double stepSize) {
+		this(numberHolder, value, minimum, maximum, stepSize, minimum);
+	}
+
+	/**
+	 * Constructor - the number holder is required.
+	 */
+	public NumberSpinnerModelAdapter(PropertyValueModel numberHolder, double value, double minimum, double maximum, double stepSize, double defaultValue) {
+		this(numberHolder, new Double(minimum), new Double(maximum), new Double(stepSize), new Double(defaultValue));
+	}
+
+	/**
+	 * Constructor - the number holder is required.
+	 */
+	public NumberSpinnerModelAdapter(PropertyValueModel numberHolder, Comparable minimum, Comparable maximum, Number stepSize, Number defaultValue) {
+		super(numberHolder.getValue() == null ? defaultValue : (Number) numberHolder.getValue(), minimum, maximum, stepSize);
+		this.numberHolder = numberHolder;
+		this.numberChangeListener = this.buildNumberChangeListener();
+		// postpone listening to the underlying number
+		// until we have listeners ourselves...
+		this.defaultValue = defaultValue;
+	}
+
+
+	// ********** initialization **********
+
+	private PropertyChangeListener buildNumberChangeListener() {
+		return new PropertyChangeListener() {
+			public void propertyChanged(PropertyChangeEvent e) {
+				NumberSpinnerModelAdapter.this.synchronize(e.newValue());
+			}
+			@Override
+			public String toString() {
+				return "number listener";
+			}
+		};
+	}
+
+
+	// ********** SpinnerModel implementation **********
+
+	/**
+	 * Extend to check whether this method is being called before we 
+	 * have any listeners.
+	 * This is necessary because some crappy jdk code gets the value
+	 * from the model *before* listening to the model.  ~bjv
+	 * @see javax.swing.JSpinner.DefaultEditor(javax.swing.JSpinner)
+	 */
+    @Override
+	public Object getValue() {
+		if (this.getChangeListeners().length == 0) {
+			// sorry about this "lateral" call to super  ~bjv
+			super.setValue(this.spinnerValueOf(this.numberHolder.getValue()));
+		}
+		return super.getValue();
+	}
+
+	/**
+	 * Extend to update the underlying number directly.
+	 * The resulting event will be ignored: @see #synchronizeDelegate(Object).
+	 */
+    @Override
+	public void setValue(Object value) {
+		super.setValue(value);
+		this.numberHolder.setValue(value);
+	}
+
+	/**
+	 * Extend to start listening to the underlying number if necessary.
+	 */
+    @Override
+	public void addChangeListener(ChangeListener listener) {
+		if (this.getChangeListeners().length == 0) {
+			this.numberHolder.addPropertyChangeListener(ValueModel.VALUE, this.numberChangeListener);
+			this.synchronize(this.numberHolder.getValue());
+		}
+		super.addChangeListener(listener);
+	}
+
+	/**
+	 * Extend to stop listening to the underlying number if appropriate.
+	 */
+    @Override
+	public void removeChangeListener(ChangeListener listener) {
+		super.removeChangeListener(listener);
+		if (this.getChangeListeners().length == 0) {
+			this.numberHolder.removePropertyChangeListener(ValueModel.VALUE, this.numberChangeListener);
+		}
+	}
+
+
+	// ********** queries **********
+
+	protected Number getDefaultValue() {
+		return this.defaultValue;
+	}
+
+	/**
+	 * Convert to a non-null value.
+	 */
+	protected Object spinnerValueOf(Object value) {
+		return (value == null) ? this.getDefaultValue() : value;
+	}
+
+
+	// ********** behavior **********
+
+	/**
+	 * Set the spinner value if it has changed.
+	 */
+	void synchronize(Object value) {
+		Object newValue = this.spinnerValueOf(value);
+		// check to see whether the date has already been synchronized
+		// (via #setValue())
+		if ( ! this.getValue().equals(newValue)) {
+			this.setValue(newValue);
+		}
+	}
+
+
+	// ********** standard methods **********
+
+	@Override
+	public String toString() {
+		return StringTools.buildToStringFor(this, this.numberHolder);
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/ObjectListSelectionModel.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/ObjectListSelectionModel.java
new file mode 100644
index 0000000..89b5aa3
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/ObjectListSelectionModel.java
@@ -0,0 +1,400 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.model.value.swing;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+import javax.swing.DefaultListSelectionModel;
+import javax.swing.ListModel;
+import javax.swing.event.ListDataEvent;
+import javax.swing.event.ListDataListener;
+import javax.swing.event.ListSelectionListener;
+
+import org.eclipse.jpt.utility.internal.CollectionTools;
+
+/**
+ * This ListSelectionModel is aware of the ListModel and
+ * provides convenience methods to access and set the
+ * selected *objects*, as opposed to the selected *indexes*.
+ */
+public class ObjectListSelectionModel extends DefaultListSelectionModel {
+	/** The list model referenced by the list selection model. */
+	private ListModel listModel;
+	/** A listener that allows us to clear the selection when the list model has changed. */
+	private ListDataListener listDataListener;
+
+
+	// ********** constructors **********
+
+	/**
+	 * Default constructor - private.
+	 */
+	private ObjectListSelectionModel() {
+		super();
+	}
+
+	/**
+	 * Construct a list selection model for the specified list model.
+	 */
+	public ObjectListSelectionModel(ListModel listModel) {
+		this();
+		this.listModel = listModel;
+		this.listDataListener = this.buildListDataListener();
+	}
+
+
+	// ********** initialization **********
+
+	private ListDataListener buildListDataListener() {
+		return new ListDataListener() {
+			public void intervalAdded(ListDataEvent e) {
+				// this does not affect the selection
+			}
+			public void intervalRemoved(ListDataEvent e) {
+				// this does not affect the selection
+			}
+			public void contentsChanged(ListDataEvent e) {
+				ObjectListSelectionModel.this.listModelContentsChanged(e);
+			}
+			@Override
+			public String toString() {
+				return "list data listener";
+			}
+		};
+	}
+
+	/**
+	 * Typically, the selection does not need to be cleared when the
+	 * contents of the list have changed. Most of the time this just
+	 * means an item has changed in a way that affects its display string
+	 * or icon. We typically only use the class for edits involving 
+	 * single selection.
+	 * A subclass can override this method if the selection
+	 * should be cleared because a change could mean the selection is invalid.
+	 */
+	protected void listModelContentsChanged(ListDataEvent e) {
+		/**this.clearSelection();*/
+	}
+
+
+	// ********** ListSelectionModel implementation **********
+
+	@Override
+	public void addListSelectionListener(ListSelectionListener l) {
+		if (this.hasNoListSelectionListeners()) {
+			this.listModel.addListDataListener(this.listDataListener);
+		}
+		super.addListSelectionListener(l);
+	}
+
+	@Override
+	public void removeListSelectionListener(ListSelectionListener l) {
+		super.removeListSelectionListener(l);
+		if (this.hasNoListSelectionListeners()) {
+			this.listModel.removeListDataListener(this.listDataListener);
+		}
+	}
+
+
+	// ********** queries **********
+
+	/**
+	 * Return whether this model has no listeners.
+	 */
+	protected boolean hasNoListSelectionListeners() {	// private-protected
+		return this.getListSelectionListeners().length == 0;
+	}
+	
+	/**
+	 * Return the list model referenced by the list selection model.
+	 */
+	public ListModel getListModel() {
+		return this.listModel;
+	}
+
+	public int getSelectedValuesSize() {
+		int min = this.getMinSelectionIndex();
+		int max = this.getMaxSelectionIndex();
+
+		if ((min < 0) || (max < 0)) {
+			return 0;
+		}
+
+		int n = 0;
+		int count = this.getListModel().getSize();
+		for (int i = min; i <= max; i++) {
+			if (this.isSelectedIndex(i) && (i < count)) {
+				n++;
+			}
+		}
+		return n;
+	}
+
+	/**
+	 * Return the first selected value.
+	 * Return null if the selection is empty.
+	 */
+	public Object getSelectedValue() {
+		int index = this.getMinSelectionIndex();
+		if (index == -1) {
+			return null;
+		}
+		if (this.getListModel().getSize() <= index) {
+			return null;
+		}
+		return this.getListModel().getElementAt(index);
+	}
+
+	/**
+	 * Return an array of the selected values.
+	 */
+	public Object[] getSelectedValues() {
+		int min = this.getMinSelectionIndex();
+		int max = this.getMaxSelectionIndex();
+
+		if ((min < 0) || (max < 0)) {
+			return new Object[0];
+		}
+
+		int maxSize = (max - min) + 1;
+		Object[] temp = new Object[maxSize];
+		int n = 0;
+		int count = this.getListModel().getSize();
+		for (int i = min; i <= max; i++) {
+			if (this.isSelectedIndex(i) && (i < count)) {
+				temp[n++] = this.getListModel().getElementAt(i);
+			}
+		}
+		if (n == maxSize) {
+			// all the elements in the range were selected
+			return temp;
+		}
+		// only some of the elements in the range were selected
+		Object[] result = new Object[n];
+		System.arraycopy(temp, 0, result, 0, n);
+		return result;
+	}
+
+	/**
+	 * Set the selected value.
+	 */
+	public void setSelectedValue(Object object) {
+		this.setSelectedValues(CollectionTools.singletonIterator(object));
+	}
+
+	/**
+	 * Set the current set of selected objects to the specified objects.
+	 * @see javax.swing.ListSelectionModel#setSelectionInterval(int, int)
+	 */
+	public void setSelectedValues(Iterator objects) {
+		this.setValueIsAdjusting(true);
+		this.clearSelection();
+		this.addSelectedValuesInternal(objects);
+		this.setValueIsAdjusting(false);
+	}
+
+	/**
+	 * Set the current set of selected objects to the specified objects.
+	 * @see javax.swing.ListSelectionModel#setSelectionInterval(int, int)
+	 */
+	public void setSelectedValues(Collection objects) {
+		this.setSelectedValues(objects.iterator());
+	}
+	
+	/**
+	 * Set the current set of selected objects to the specified objects.
+	 * @see javax.swing.ListSelectionModel#setSelectionInterval(int, int)
+	 */
+	public void setSelectedValues(Object[] objects) {
+		this.setSelectedValues(CollectionTools.iterator(objects));
+	}
+
+	/**
+	 * Add the specified object to the current set of selected objects.
+	 * @see javax.swing.ListSelectionModel#addSelectionInterval(int, int)
+	 */
+	public void addSelectedValue(Object object) {
+		this.addSelectedValues(CollectionTools.singletonIterator(object));
+	}
+
+	/**
+	 * Add the specified objects to the current set of selected objects.
+	 * @see javax.swing.ListSelectionModel#addSelectionInterval(int, int)
+	 */
+	public void addSelectedValues(Iterator objects) {
+		this.setValueIsAdjusting(true);
+		this.addSelectedValuesInternal(objects);
+		this.setValueIsAdjusting(false);
+	}
+	
+	/**
+	 * Add the specified objects to the current set of selected objects.
+	 * @see javax.swing.ListSelectionModel#addSelectionInterval(int, int)
+	 */
+	public void addSelectedValues(Collection objects) {
+		this.addSelectedValues(objects.iterator());
+	}
+	
+	/**
+	 * Add the specified objects to the current set of selected objects.
+	 * @see javax.swing.ListSelectionModel#addSelectionInterval(int, int)
+	 */
+	public void addSelectedValues(Object[] objects) {
+		this.addSelectedValues(CollectionTools.iterator(objects));
+	}
+	
+	/**
+	 * Remove the specified object from the current set of selected objects.
+	 * @see javax.swing.ListSelectionModel#removeSelectionInterval(int, int)
+	 */
+	public void removeSelectedValue(Object object) {
+		this.removeSelectedValues(CollectionTools.singletonIterator(object));
+	}
+
+	/**
+	 * Remove the specified objects from the current set of selected objects.
+	 * @see javax.swing.ListSelectionModel#removeSelectionInterval(int, int)
+	 */
+	public void removeSelectedValues(Iterator objects) {
+		this.setValueIsAdjusting(true);
+		ListModel lm = this.getListModel();
+		int lmSize = lm.getSize();
+		while (objects.hasNext()) {
+			int index = this.indexOf(objects.next(), lm, lmSize);
+			this.removeSelectionInterval(index, index);
+		}
+		this.setValueIsAdjusting(false);
+	}
+	
+	/**
+	 * Remove the specified objects from the current set of selected objects.
+	 * @see javax.swing.ListSelectionModel#removeSelectionInterval(int, int)
+	 */
+	public void removeSelectedValues(Collection objects) {
+		this.removeSelectedValues(objects.iterator());
+	}
+	
+	/**
+	 * Remove the specified objects from the current set of selected objects.
+	 * @see javax.swing.ListSelectionModel#removeSelectionInterval(int, int)
+	 */
+	public void removeSelectedValues(Object[] objects) {
+		this.removeSelectedValues(CollectionTools.iterator(objects));
+	}
+	
+	/**
+	 * @see javax.swing.ListSelectionModel#getAnchorSelectionIndex()
+	 * Return null if the anchor selection is empty.
+	 */
+	public Object getAnchorSelectedValue() {
+		int index = this.getAnchorSelectionIndex();
+		if (index == -1) {
+			return null;
+		}
+		return this.getListModel().getElementAt(index);
+	}
+	
+	/**
+	 * @see javax.swing.ListSelectionModel#setAnchorSelectionIndex(int)
+	 */
+	public void setAnchorSelectedValue(Object object) {
+		this.setAnchorSelectionIndex(this.indexOf(object));
+	}
+	
+	/**
+	 * @see javax.swing.ListSelectionModel#getLeadSelectionIndex()
+	 * Return null if the lead selection is empty.
+	 */
+	public Object getLeadSelectedValue() {
+		int index = this.getLeadSelectionIndex();
+		if (index == -1) {
+			return null;
+		}
+		return this.getListModel().getElementAt(index);
+	}
+	
+	/**
+	 * @see javax.swing.ListSelectionModel#setLeadSelectionIndex(int)
+	 */
+	public void setLeadSelectedValue(Object object) {
+		this.setLeadSelectionIndex(this.indexOf(object));
+	}
+	
+	/**
+	 * @see javax.swing.ListSelectionModel#getMaxSelectionIndex()
+	 * Return null if the max selection is empty.
+	 */
+	public Object getMaxSelectedValue() {
+		int index = this.getMaxSelectionIndex();
+		if (index == -1) {
+			return null;
+		}
+		return this.getListModel().getElementAt(index);
+	}
+	
+	/**
+	 * @see javax.swing.ListSelectionModel#getMinSelectionIndex()
+	 * Return null if the min selection is empty.
+	 */
+	public Object getMinSelectedValue() {
+		int index = this.getMinSelectionIndex();
+		if (index == -1) {
+			return null;
+		}
+		return this.getListModel().getElementAt(index);
+	}
+
+	/**
+	 * @see javax.swing.ListSelectionModel#isSelectedIndex(int)
+	 */
+	public boolean valueIsSelected(Object object) {
+		return this.isSelectedIndex(this.indexOf(object));
+	}
+
+	/**
+	 * Add the specified objects to the current set of selected objects,
+	 * without wrapping the actions in "adjusting" events.
+	 */
+	private void addSelectedValuesInternal(Iterator objects) {
+		ListModel lm = this.getListModel();
+		int listModelSize = lm.getSize();
+		while (objects.hasNext()) {
+			int index = this.indexOf(objects.next(), lm, listModelSize);
+			this.addSelectionInterval(index, index);
+		}
+	}
+
+	/**
+	 * Return the index in the list model of the specified object.
+	 * Return -1 if the object is not in the list model.
+	 */
+	private int indexOf(Object object) {
+		ListModel lm = this.getListModel();
+		return this.indexOf(object, lm, lm.getSize());
+	}
+
+	/**
+	 * Return the index in the list model of the specified object.
+	 * Return -1 if the object is not in the list model.
+	 */
+	// we're just jerking around with performance optimizations here
+	// (in memory of Phil...);
+	// call this method inside loops that do not modify the listModel
+	private int indexOf(Object object, ListModel lm, int listModelSize) {
+		for (int i = listModelSize; i-- > 0; ) {
+			if (lm.getElementAt(i) == object) {
+				return i;
+			}
+		}
+		return -1;
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/PrimitiveListTreeModel.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/PrimitiveListTreeModel.java
new file mode 100644
index 0000000..ca797bb
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/PrimitiveListTreeModel.java
@@ -0,0 +1,240 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.model.value.swing;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.ListIterator;
+
+import javax.swing.event.TreeModelListener;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.DefaultTreeModel;
+import javax.swing.tree.MutableTreeNode;
+import javax.swing.tree.TreeNode;
+import javax.swing.tree.TreePath;
+
+import org.eclipse.jpt.utility.internal.model.event.ListChangeEvent;
+import org.eclipse.jpt.utility.internal.model.listener.ListChangeListener;
+import org.eclipse.jpt.utility.internal.model.value.ListValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+
+/**
+ * This TreeModel implementation provides a tree with a "null" root that
+ * has a set of "primitive" children. These "primitive" children do not have
+ * children themselves, making the tree a maximum of 2 levels deep.
+ * This model automatically synchronizes the root's children with a
+ * ListValueModel that holds a collection of primitive (non-model) objects
+ * (e.g. Strings).
+ * 
+ * This is useful for providing an "editable" list of primitives. Since the JDK
+ * does not provide us with an editable listbox, we must use an editable tree.
+ * We wrap everything in DefaultMutableTreeNodes.
+ * 
+ * Subclasses must implement #primitiveChanged(int, Object) and update
+ * the model appropriately. This method is called when the user edits the
+ * list directly and presses <Enter>.
+ * 
+ * The JTree using this model must be configured as "editable":
+ * 	tree.setEditable(true);
+ */
+// TODO convert to use an adapter instead of requiring subclass
+public abstract class PrimitiveListTreeModel extends DefaultTreeModel {
+
+	/** a model on the list of primitives */
+	private ListValueModel listHolder;
+
+	/** a listener that handles the adding, removing, and replacing of the primitives */
+	private ListChangeListener listChangeListener;
+
+
+	// ********** constructors **********
+	
+	/**
+	 * Default constructor - initialize
+	 */
+	private PrimitiveListTreeModel() {
+		super(new DefaultMutableTreeNode(null, true));	// the root can have children
+		this.initialize();
+	}
+
+	/**
+	 * Public constructor - the list holder is required
+	 */
+	public PrimitiveListTreeModel(ListValueModel listHolder) {
+		this();
+		if (listHolder == null) {
+			throw new NullPointerException();
+		}
+		this.listHolder = listHolder;
+		// postpone listening to the model until we have listeners ourselves
+	}
+
+
+	// ********** initialization **********
+
+	private void initialize() {
+		this.listChangeListener = new PrimitiveListChangeListener();
+	}
+
+
+	// ********** behavior **********
+
+	/**
+	 * Subclasses should override this method to update the 
+	 * model appropriately. The primitive at the specified index was
+	 * edited directly by the user and the new value is as specified.
+	 * Convert the value appropriately and place it in the model.
+	 */
+	protected abstract void primitiveChanged(int index, Object newValue);
+
+
+	// ********** TreeModel implementation **********
+
+	/**
+	 * Override to change the underlying model instead of changing the node directly.
+	 */
+    @Override
+	public void valueForPathChanged(TreePath path, Object newValue) {
+		TreeNode node = (TreeNode) path.getLastPathComponent();
+		int index = ((TreeNode) this.getRoot()).getIndex(node);
+		this.primitiveChanged(index, newValue);
+	}
+
+	/**
+	 * Extend to start listening to the underlying model if necessary.
+	 */
+    @Override
+	public void addTreeModelListener(TreeModelListener l) {
+		if (this.getTreeModelListeners().length == 0) {
+			this.listHolder.addListChangeListener(ValueModel.VALUE, this.listChangeListener);
+			this.synchronizeList();
+		}
+		super.addTreeModelListener(l);
+	}
+
+	/**
+	 * Extend to stop listening to the underlying model if appropriate.
+	 */
+    @Override
+	public void removeTreeModelListener(TreeModelListener l) {
+		super.removeTreeModelListener(l);
+		if (this.getTreeModelListeners().length == 0) {
+			this.listHolder.removeListChangeListener(ValueModel.VALUE, this.listChangeListener);
+		}
+	}
+
+
+	// ********** behavior **********
+
+	/**
+	 * Synchronize our list of nodes with the list of primitives
+	 */
+	void synchronizeList() {
+		this.clearList();
+		this.buildList();
+	}
+
+	void clearList() {
+		int childcount = this.root.getChildCount();
+		for (int i = childcount - 1; i >= 0; i--) {
+			this.removeNodeFromParent((MutableTreeNode)this.root.getChildAt(i));
+		}
+	}
+		
+	private void buildList() {
+		for (Iterator stream = (Iterator) this.listHolder.getValue(); stream.hasNext(); ) {
+			this.addPrimitive(stream.next());
+		}
+	}
+
+	/**
+	 * Add the specified primitive to the end of the list.
+	 */
+	private void addPrimitive(Object primitive) {
+		this.insertPrimitive(this.root.getChildCount(), primitive);
+	}
+
+	/**
+	 * Create a node for the specified primitive
+	 * and insert it as a child of the root.
+	 */
+	void insertPrimitive(int index, Object primitive) {
+		DefaultMutableTreeNode node = new DefaultMutableTreeNode(primitive, false); // don't allow children on the child node
+		this.insertNodeInto(node, (MutableTreeNode) this.root, index);
+	}
+
+	/**
+	 * Remove node at the specified index.
+	 */
+	MutableTreeNode removeNode(int index) {
+		MutableTreeNode node = (MutableTreeNode) this.root.getChildAt(index);
+		this.removeNodeFromParent(node);
+		return node;
+	}
+
+	/**
+	 * Replace the user object of the node at childIndex.
+	 */
+	void replacePrimitive(int index, Object primitive) {
+		MutableTreeNode node = (MutableTreeNode) this.root.getChildAt(index);
+		node.setUserObject(primitive);
+		this.nodeChanged(node);
+	}
+
+
+	// ********** inner class **********
+
+	private class PrimitiveListChangeListener implements ListChangeListener {
+		PrimitiveListChangeListener() {
+			super();
+		}
+
+		public void itemsAdded(ListChangeEvent e) {
+			int i = e.index();
+			for (ListIterator stream = e.items(); stream.hasNext(); ) {
+				PrimitiveListTreeModel.this.insertPrimitive(i++, stream.next());
+			}
+		}
+
+		public void itemsRemoved(ListChangeEvent e) {
+			for (int i = 0; i < e.itemsSize(); i++) {
+				PrimitiveListTreeModel.this.removeNode(e.index());
+			}
+		}
+
+		public void itemsReplaced(ListChangeEvent e) {
+			int i = e.index();
+			for (ListIterator stream = e.items(); stream.hasNext(); ) {
+				PrimitiveListTreeModel.this.replacePrimitive(i++, stream.next());
+			}
+		}
+
+		public void itemsMoved(ListChangeEvent e) {
+			ArrayList<MutableTreeNode> temp = new ArrayList<MutableTreeNode>(e.moveLength());
+			for (int i = 0; i < e.moveLength(); i++) {
+				temp.add(PrimitiveListTreeModel.this.removeNode(e.sourceIndex()));
+			}
+			int i = e.targetIndex();
+			for (MutableTreeNode node : temp) {
+				PrimitiveListTreeModel.this.insertPrimitive(i++, node);
+			}
+		}
+
+		public void listCleared(ListChangeEvent e) {
+			PrimitiveListTreeModel.this.clearList();
+		}
+
+		public void listChanged(ListChangeEvent e) {
+			PrimitiveListTreeModel.this.synchronizeList();
+		}
+
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/RadioButtonModelAdapter.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/RadioButtonModelAdapter.java
new file mode 100644
index 0000000..9dc34ae
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/RadioButtonModelAdapter.java
@@ -0,0 +1,150 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.model.value.swing;
+
+import org.eclipse.jpt.utility.internal.BidiFilter;
+import org.eclipse.jpt.utility.internal.BidiTransformer;
+import org.eclipse.jpt.utility.internal.model.value.FilteringPropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.TransformationPropertyValueModel;
+
+/**
+ * This javax.swing.ButtonModel can be used to keep a listener
+ * (e.g. a JRadioButton) in synch with a (typically shared)
+ * PropertyValueModel that holds one value out of a set of values.
+ * 
+ * NOTE: Do *not* use this model with a ButtonGroup, since the
+ * shared value holder and the wrappers built by this adapter will
+ * keep the appropriate radio button checked. Also, this allows
+ * us to uncheck all the radio buttons in a group when the shared
+ * value is null.
+ */
+public class RadioButtonModelAdapter extends ToggleButtonModelAdapter {
+
+
+	// ********** constructors **********
+
+	/**
+	 * Constructor - the value holder is required.
+	 */
+	public RadioButtonModelAdapter(PropertyValueModel valueHolder, Object buttonValue, boolean defaultValue) {
+		super(buildBooleanHolder(valueHolder, buttonValue), defaultValue);
+	}
+
+	/**
+	 * Constructor - the value holder is required.
+	 * The default value will be false.
+	 */
+	public RadioButtonModelAdapter(PropertyValueModel valueHolder, Object buttonValue) {
+		super(buildBooleanHolder(valueHolder, buttonValue));
+	}
+
+
+	// ********** static methods **********
+
+	/**
+	 * Build up a set of wrappers that will convert the
+	 * specified value holder and button value to/from a boolean.
+	 * 
+	 * If the value holder's value matches the button value,
+	 * the wrapper will return true. Likewise, if the value holder's
+	 * value is set to true, the wrapper will set the value holder's
+	 * value to the button value.
+	 */
+	public static PropertyValueModel buildBooleanHolder(PropertyValueModel valueHolder, Object buttonValue) {
+		PropertyValueModel filteringPVM = new FilteringPropertyValueModel(valueHolder, new RadioButtonFilter(buttonValue));
+		return new TransformationPropertyValueModel(filteringPVM, new RadioButtonTransformer(buttonValue));
+	}
+
+
+	// ********** overrides **********
+
+	/**
+	 * The user cannot de-select a radio button - the user
+	 * can only *select* a radio button. Only the model can
+	 * cause a radio button to be de-selected. We use the
+	 * ARMED flag to indicate whether we are being de-selected
+	 * by the user.
+	 */
+    @Override
+	public void setSelected(boolean b) {
+		// do not allow the user to de-select a radio button
+		// radio buttons can
+		if ((b == false) && this.isArmed()) {
+			return;
+		}
+		super.setSelected(b);
+	}
+
+
+	// ********** inner classes **********
+
+	/**
+	 * This filter will only pass through a new value to the wrapped
+	 * value holder when it matches the configured button value.
+	 */
+	public static class RadioButtonFilter implements BidiFilter {
+		private Object buttonValue;
+
+		public RadioButtonFilter(Object buttonValue) {
+			super();
+			this.buttonValue = buttonValue;
+		}
+
+		/**
+		 * always return the wrapped value
+		 */
+		public boolean accept(Object value) {
+			return true;
+		}
+
+		/**
+		 * pass through the value to the wrapped property value model
+		 * *only* when it matches our button value
+		 */
+		public boolean reverseAccept(Object value) {
+			return value == this.buttonValue;
+		}
+
+	}
+
+	/**
+	 * This transformer will convert the wrapped value to Boolean.TRUE
+	 * when it matches the configured button value.
+	 */
+	public static class RadioButtonTransformer implements BidiTransformer {
+		private Object buttonValue;
+
+		public RadioButtonTransformer(Object buttonValue) {
+			super();
+			this.buttonValue = buttonValue;
+		}
+
+		/**
+		 * if the wrapped value matches our button value return true,
+		 * if it is some other value return false;
+		 * but if it is null simply pass it through because it will cause the
+		 * button model's default value to be used
+		 */
+		public Object transform(Object value) {
+			return (value == null) ? null : Boolean.valueOf(value == this.buttonValue);
+		}
+
+		/**
+		 * if the new value is true, pass through the our button value;
+		 * otherwise pass through null
+		 */
+		public Object reverseTransform(Object value) {
+			return (((Boolean) value).booleanValue()) ? this.buttonValue : null;
+		}
+
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/SpinnerModelAdapter.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/SpinnerModelAdapter.java
new file mode 100644
index 0000000..9e8f805
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/SpinnerModelAdapter.java
@@ -0,0 +1,213 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.model.value.swing;
+
+import javax.swing.AbstractSpinnerModel;
+import javax.swing.SpinnerModel;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import org.eclipse.jpt.utility.internal.StringTools;
+import org.eclipse.jpt.utility.internal.model.event.PropertyChangeEvent;
+import org.eclipse.jpt.utility.internal.model.listener.PropertyChangeListener;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+
+/**
+ * This javax.swing.SpinnerModel can be used to keep a ChangeListener
+ * (e.g. a JSpinner) in synch with a PropertyValueModel that holds a value.
+ * 
+ * Note: it is likely you want to use one of the following classes instead of
+ * this one:
+ *     DateSpinnerModelAdapter
+ *     NumberSpinnerModelAdapter
+ *     ListSpinnerModelAdapter
+ * 
+ * NB: This model should only be used for values that have a fairly
+ * inexpensive #equals() implementation.
+ * @see #synchronizeDelegate(Object)
+ */
+public class SpinnerModelAdapter extends AbstractSpinnerModel {
+
+	/** The delegate spinner model whose behavior we "enhance". */
+	protected SpinnerModel delegate;
+
+	/** A listener that allows us to forward any changes made to the delegate spinner model. */
+	protected ChangeListener delegateListener;
+
+	/** A value model on the underlying value. */
+	protected PropertyValueModel valueHolder;
+
+	/** A listener that allows us to synchronize with changes made to the underlying value. */
+	protected PropertyChangeListener valueListener;
+
+
+	// ********** constructors **********
+
+	/**
+	 * Default constructor - initialize stuff.
+	 */
+	private SpinnerModelAdapter() {
+		super();
+		this.initialize();
+	}
+
+	/**
+	 * Constructor - the value holder and delegate are required.
+	 */
+	public SpinnerModelAdapter(PropertyValueModel valueHolder, SpinnerModel delegate) {
+		this();
+		if (valueHolder == null || delegate == null) {
+			throw new NullPointerException();
+		}
+		this.valueHolder = valueHolder;
+		this.delegate = delegate;
+		// postpone listening to the underlying value
+		// until we have listeners ourselves...
+	}
+
+	/**
+	 * Constructor - the value holder is required.
+	 * This will wrap a simple number spinner model.
+	 */
+	public SpinnerModelAdapter(PropertyValueModel valueHolder) {
+		this(valueHolder, new SpinnerNumberModel());
+	}
+
+
+	// ********** initialization **********
+
+	protected void initialize() {
+		this.valueListener = this.buildValueListener();
+		this.delegateListener = this.buildDelegateListener();
+	}
+
+	protected PropertyChangeListener buildValueListener() {
+		return new PropertyChangeListener() {
+			public void propertyChanged(PropertyChangeEvent e) {
+				SpinnerModelAdapter.this.valueChanged(e);
+			}
+			@Override
+			public String toString() {
+				return "value listener";
+			}
+		};
+	}
+
+	/**
+	 * expand access a bit for inner class
+	 */
+	@Override
+	protected void fireStateChanged() {
+		super.fireStateChanged();
+	}
+
+	protected ChangeListener buildDelegateListener() {
+		return new ChangeListener() {
+			public void stateChanged(ChangeEvent e) {
+				// forward the event, with this as the source
+				SpinnerModelAdapter.this.fireStateChanged();
+			}
+			@Override
+			public String toString() {
+				return "delegate listener";
+			}
+		};
+	}
+
+
+	// ********** SpinnerModel implementation **********
+
+	public Object getValue() {
+		return this.delegate.getValue();
+	}
+
+	/**
+	 * Extend to update the underlying value directly.
+	 * The resulting event will be ignored: @see #synchronizeDelegate(Object).
+	 */
+	public void setValue(Object value) {
+		this.delegate.setValue(value);
+		this.valueHolder.setValue(value);
+	}
+
+	public Object getNextValue() {
+		return this.delegate.getNextValue();
+	}
+
+	public Object getPreviousValue() {
+		return this.delegate.getPreviousValue();
+	}
+
+	/**
+	 * Extend to start listening to the underlying value if necessary.
+	 */
+    @Override
+	public void addChangeListener(ChangeListener listener) {
+		if (this.listenerList.getListenerCount(ChangeListener.class) == 0) {
+			this.delegate.addChangeListener(this.delegateListener);
+			this.engageValueHolder();
+		}
+		super.addChangeListener(listener);
+	}
+
+	/**
+	 * Extend to stop listening to the underlying value if appropriate.
+	 */
+    @Override
+	public void removeChangeListener(ChangeListener listener) {
+		super.removeChangeListener(listener);
+		if (this.listenerList.getListenerCount(ChangeListener.class) == 0) {
+			this.disengageValueHolder();
+			this.delegate.removeChangeListener(this.delegateListener);
+		}
+	}
+
+
+	// ********** behavior **********
+
+	/**
+	 * A third party has modified the underlying value.
+	 * Synchronize the delegate model accordingly.
+	 */
+	protected void valueChanged(PropertyChangeEvent e) {
+		this.synchronizeDelegate(e.newValue());
+	}
+
+	/**
+	 * Set the delegate's value if it has changed.
+	 */
+	protected void synchronizeDelegate(Object value) {
+		// check to see whether the delegate has already been synchronized
+		// (via #setValue())
+		if ( ! this.delegate.getValue().equals(value)) {
+			this.delegate.setValue(value);
+		}
+	}
+
+	protected void engageValueHolder() {
+		this.valueHolder.addPropertyChangeListener(ValueModel.VALUE, this.valueListener);
+		this.synchronizeDelegate(this.valueHolder.getValue());
+	}
+
+	protected void disengageValueHolder() {
+		this.valueHolder.removePropertyChangeListener(ValueModel.VALUE, this.valueListener);
+	}
+
+
+	// ********** standard methods **********
+
+	@Override
+	public String toString() {
+		return StringTools.buildToStringFor(this, this.valueHolder);
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/TableModelAdapter.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/TableModelAdapter.java
new file mode 100644
index 0000000..0bb0929
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/TableModelAdapter.java
@@ -0,0 +1,402 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.model.value.swing;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.swing.event.TableModelListener;
+import javax.swing.table.AbstractTableModel;
+
+import org.eclipse.jpt.utility.internal.model.event.ListChangeEvent;
+import org.eclipse.jpt.utility.internal.model.event.PropertyChangeEvent;
+import org.eclipse.jpt.utility.internal.model.listener.ListChangeListener;
+import org.eclipse.jpt.utility.internal.model.listener.PropertyChangeListener;
+import org.eclipse.jpt.utility.internal.model.value.CollectionListValueModelAdapter;
+import org.eclipse.jpt.utility.internal.model.value.CollectionValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ListValueModel;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+
+/**
+ * This TableModel can be used to keep a TableModelListener (e.g. a JTable)
+ * in synch with a ListValueModel that holds a collection of model objects,
+ * each of which corresponds to a row in the table.
+ * Typically, each column of the table will be bound to a different aspect
+ * of the contained model objects.
+ * 
+ * For example, a MWTable has an attribute 'databaseFields' that holds
+ * a collection of MWDatabaseFields that would correspond to the rows of
+ * a JTable; and each MWDatabaseField has a number
+ * of attributes (e.g. name, type, size) that can be bound to the columns of
+ * a row in the JTable. As these database fields are added, removed, and
+ * changed, this model will keep the listeners aware of the changes.
+ * 
+ * An instance of this TableModel must be supplied with a
+ * list holder (e.g. the 'databaseFields'), which is a value
+ * model on the bound collection This is required - the
+ * collection itself can be null, but the list value model that
+ * holds it is required. Typically this list will be sorted (@see
+ * SortedListValueModelAdapter).
+ * 
+ * This TableModel must also be supplied with a ColumnAdapter that
+ * will be used to configure the headers, renderers, editors, and contents
+ * of the various columns.
+ * 
+ * Design decision:
+ * Cell listener options (from low space/high time to high space/low time):
+ * 	- 1 cell listener listening to every cell (this is the current implementation)
+ * 	- 1 cell listener per row
+ * 	- 1 cell listener per cell
+ */
+public class TableModelAdapter extends AbstractTableModel {
+
+	/**
+	 * a list of user objects that are converted to
+	 * rows via the column adapter
+	 */
+	private ListValueModel listHolder;
+	private ListChangeListener listChangeListener;
+
+	/**
+	 * each row is an array of cell models
+	 */
+	private ArrayList<PropertyValueModel[]> rows;	// declare as ArrayList so we can use #ensureCapacity(int)
+
+	/**
+	 * client-supplied adapter that provides with the various column
+	 * settings and converts the objects in the LVM
+	 * into an array of cell models
+	 */
+	private ColumnAdapter columnAdapter;
+
+	/**
+	 * the single listener that listens to every cell's model
+	 */
+	private PropertyChangeListener cellListener;
+
+
+	// ********** constructors **********
+
+	/**
+	 * internal constructor
+	 */
+	private TableModelAdapter() {
+		super();
+		this.initialize();
+	}
+
+	/**
+	 * Construct a table model adapter for the specified objects
+	 * and adapter.
+	 */
+	public TableModelAdapter(ListValueModel listHolder, ColumnAdapter columnAdapter) {
+		this();
+		if (listHolder == null) {
+			throw new NullPointerException();
+		}
+		this.listHolder = listHolder;
+		this.columnAdapter = columnAdapter;
+	}
+
+	/**
+	 * Construct a table model adapter for the specified objects
+	 * and adapter.
+	 */
+	public TableModelAdapter(CollectionValueModel collectionHolder, ColumnAdapter columnAdapter) {
+		this(new CollectionListValueModelAdapter(collectionHolder), columnAdapter);
+	}
+
+
+	// ********** initialization **********
+
+	private void initialize() {
+		this.listChangeListener = this.buildListChangeListener();
+		this.rows = new ArrayList<PropertyValueModel[]>();
+		this.cellListener = this.buildCellListener();
+	}
+
+	private ListChangeListener buildListChangeListener() {
+		return new ListChangeListener() {
+			public void itemsAdded(ListChangeEvent e) {
+				TableModelAdapter.this.addRows(e.index(), e.itemsSize(), e.items());
+			}
+			public void itemsRemoved(ListChangeEvent e) {
+				TableModelAdapter.this.removeRows(e.index(), e.itemsSize());
+			}
+			public void itemsReplaced(ListChangeEvent e) {
+				TableModelAdapter.this.replaceRows(e.index(), e.items());
+			}
+			public void itemsMoved(ListChangeEvent e) {
+				TableModelAdapter.this.moveRows(e.targetIndex(), e.sourceIndex(), e.moveLength());
+			}
+			public void listCleared(ListChangeEvent e) {
+				TableModelAdapter.this.clearTable();
+			}
+			public void listChanged(ListChangeEvent e) {
+				TableModelAdapter.this.rebuildTable();
+			}
+			@Override
+			public String toString() {
+				return "list listener";
+			}
+		};
+	}
+
+	private PropertyChangeListener buildCellListener() {
+		return new PropertyChangeListener() {
+			public void propertyChanged(PropertyChangeEvent evt) {
+				TableModelAdapter.this.cellChanged((PropertyValueModel) evt.getSource());
+			}
+			@Override
+			public String toString() {
+				return "cell listener";
+			}
+		};
+	}
+
+
+	// ********** TableModel implementation **********
+
+	public int getColumnCount() {
+		return this.columnAdapter.getColumnCount();
+	}
+
+	public int getRowCount() {
+		return this.rows.size();
+	}
+
+    @Override
+	public String getColumnName(int column) {
+		return this.columnAdapter.getColumnName(column);
+	}
+
+    @Override
+	public Class getColumnClass(int columnIndex) {
+		return this.columnAdapter.getColumnClass(columnIndex);
+	}
+
+    @Override
+	public boolean isCellEditable(int rowIndex, int columnIndex) {
+		return this.columnAdapter.isColumnEditable(columnIndex);
+	}
+
+	public Object getValueAt(int rowIndex, int columnIndex) {
+		PropertyValueModel[] row = this.rows.get(rowIndex);
+		return row[columnIndex].getValue();
+	}
+
+	@Override
+	public void setValueAt(Object value, int rowIndex, int columnIndex) {
+		PropertyValueModel[] row = this.rows.get(rowIndex);
+		row[columnIndex].setValue(value);
+	}
+
+	/**
+	 * Extend to start listening to the underlying model if necessary.
+	 */
+    @Override
+	public void addTableModelListener(TableModelListener l) {
+		if (this.hasNoTableModelListeners()) {
+			this.engageModel();
+		}
+		super.addTableModelListener(l);
+	}
+
+	/**
+	 * Extend to stop listening to the underlying model if necessary.
+	 */
+    @Override
+	public void removeTableModelListener(TableModelListener l) {
+		super.removeTableModelListener(l);
+		if (this.hasNoTableModelListeners()) {
+			this.disengageModel();
+		}
+	}
+
+
+	// ********** public API **********
+
+	/**
+	 * Return the underlying list model.
+	 */
+	public ListValueModel getModel() {
+		return this.listHolder;
+	}
+
+	/**
+	 * Set the underlying list model.
+	 */
+	public void setModel(ListValueModel listHolder) {
+		if (listHolder == null) {
+			throw new NullPointerException();
+		}
+		boolean hasListeners = this.hasTableModelListeners();
+		if (hasListeners) {
+			this.disengageModel();
+		}
+		this.listHolder = listHolder;
+		if (hasListeners) {
+			this.engageModel();
+			this.fireTableDataChanged();
+		}
+	}
+
+	/**
+	 * Set the underlying collection model.
+	 */
+	public void setModel(CollectionValueModel collectionHolder) {
+		this.setModel(new CollectionListValueModelAdapter(collectionHolder));
+	}
+
+
+	// ********** queries **********
+
+	/**
+	 * Return whether this model has no listeners.
+	 */
+	protected boolean hasNoTableModelListeners() {
+		return this.listenerList.getListenerCount(TableModelListener.class) == 0;
+	}
+
+	/**
+	 * Return whether this model has any listeners.
+	 */
+	protected boolean hasTableModelListeners() {
+		return ! this.hasNoTableModelListeners();
+	}
+
+
+	// ********** behavior **********
+
+	/**
+	 * Start listening to the list of objects and the various aspects
+	 * of the objects that make up the rows.
+	 */
+	private void engageModel() {
+		this.listHolder.addListChangeListener(ValueModel.VALUE, this.listChangeListener);
+		this.engageAllCells();
+	}
+
+	/**
+	 * Convert the objects into rows and listen to the cells.
+	 */
+	private void engageAllCells() {
+		this.rows.ensureCapacity(this.listHolder.size());
+		for (Iterator stream = (Iterator) this.listHolder.getValue(); stream.hasNext(); ) {
+			PropertyValueModel[] row = this.columnAdapter.cellModels(stream.next());
+			this.engageRow(row);
+			this.rows.add(row);
+		}
+	}
+
+	/**
+	 * Listen to the cells in the specified row.
+	 */
+	private void engageRow(PropertyValueModel[] row) {
+		for (int i = row.length; i-- > 0; ) {
+			row[i].addPropertyChangeListener(ValueModel.VALUE, this.cellListener);
+		}
+	}
+
+	/**
+	 * Stop listening.
+	 */
+	private void disengageModel() {
+		this.disengageAllCells();
+		this.listHolder.removeListChangeListener(ValueModel.VALUE, this.listChangeListener);
+	}
+
+	private void disengageAllCells() {
+		for (PropertyValueModel[] row : this.rows) {
+			this.disengageRow(row);
+		}
+		this.rows.clear();
+	}
+
+	private void disengageRow(PropertyValueModel[] row) {
+		for (int i = row.length; i-- > 0; ) {
+			row[i].removePropertyChangeListener(ValueModel.VALUE, this.cellListener);
+		}
+	}
+
+	/**
+	 * brute-force search for the cell(s) that changed...
+	 */
+	void cellChanged(PropertyValueModel cellHolder) {
+		for (int i = this.rows.size(); i-- > 0; ) {
+			PropertyValueModel[] row = this.rows.get(i);
+			for (int j = row.length; j-- > 0; ) {
+				if (row[j] == cellHolder) {
+					this.fireTableCellUpdated(i, j);
+				}
+			}
+		}
+	}
+
+	/**
+	 * convert the items to rows
+	 */
+	void addRows(int index, int size, Iterator items) {
+		List<PropertyValueModel[]> newRows = new ArrayList<PropertyValueModel[]>(size);
+		while (items.hasNext()) {
+			PropertyValueModel[] row = this.columnAdapter.cellModels(items.next());
+			this.engageRow(row);
+			newRows.add(row);
+		}
+		this.rows.addAll(index, newRows);
+		this.fireTableRowsInserted(index, index + size - 1);
+	}
+
+	void removeRows(int index, int size) {
+		for (int i = 0; i < size; i++) {
+			this.disengageRow(this.rows.remove(index));
+		}
+		this.fireTableRowsDeleted(index, index + size - 1);
+	}
+
+	void replaceRows(int index, Iterator items) {
+		int i = index;
+		while (items.hasNext()) {
+			PropertyValueModel[] row = this.rows.get(i);
+			this.disengageRow(row);
+			row = this.columnAdapter.cellModels(items.next());
+			this.engageRow(row);
+			this.rows.set(i, row);
+			i++;
+		}
+		this.fireTableRowsUpdated(index, i - 1);
+	}
+
+	void moveRows(int targetIndex, int sourceIndex, int length) {
+		ArrayList<PropertyValueModel[]> temp = new ArrayList<PropertyValueModel[]>(length);
+		for (int i = 0; i < length; i++) {
+			temp.add(this.rows.remove(sourceIndex));
+		}
+		this.rows.addAll(targetIndex, temp);
+
+		int start = Math.min(targetIndex, sourceIndex);
+		int end = Math.max(targetIndex, sourceIndex) + length - 1;
+		this.fireTableRowsUpdated(start, end);
+	}
+
+	void clearTable() {
+		this.disengageAllCells();
+		this.fireTableDataChanged();
+	}
+
+	void rebuildTable() {
+		this.disengageAllCells();
+		this.engageAllCells();
+		this.fireTableDataChanged();
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/ToggleButtonModelAdapter.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/ToggleButtonModelAdapter.java
new file mode 100644
index 0000000..01ad9d5
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/ToggleButtonModelAdapter.java
@@ -0,0 +1,231 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.model.value.swing;
+
+import java.awt.event.ActionListener;
+import java.awt.event.ItemListener;
+
+import javax.swing.JToggleButton.ToggleButtonModel;
+import javax.swing.event.ChangeListener;
+
+import org.eclipse.jpt.utility.internal.StringTools;
+import org.eclipse.jpt.utility.internal.model.event.PropertyChangeEvent;
+import org.eclipse.jpt.utility.internal.model.listener.PropertyChangeListener;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+
+/**
+ * This javax.swing.ButtonModel can be used to keep a listener
+ * (e.g. a JCheckBox or a JRadioButton) in synch with a PropertyValueModel
+ * on a boolean.
+ */
+public class ToggleButtonModelAdapter extends ToggleButtonModel {
+
+	/**
+	 * The default setting for the toggle button; for when the underlying model is null.
+	 * The default [default value] is false (i.e. the toggle button is unchecked/empty).
+	 */
+	protected boolean defaultValue;
+
+	/** A value model on the underlying model boolean. */
+	protected PropertyValueModel booleanHolder;
+
+	/**
+	 * A listener that allows us to synchronize with
+	 * changes made to the underlying model boolean.
+	 */
+	protected PropertyChangeListener booleanChangeListener;
+
+
+	// ********** constructors **********
+
+	/**
+	 * Default constructor - initialize stuff.
+	 */
+	private ToggleButtonModelAdapter() {
+		super();
+		this.initialize();
+	}
+
+	/**
+	 * Constructor - the boolean holder is required.
+	 */
+	public ToggleButtonModelAdapter(PropertyValueModel booleanHolder, boolean defaultValue) {
+		this();
+		if (booleanHolder == null) {
+			throw new NullPointerException();
+		}
+		this.booleanHolder = booleanHolder;
+		// postpone listening to the underlying model
+		// until we have listeners ourselves...
+		this.defaultValue = defaultValue;
+	}
+
+	/**
+	 * Constructor - the boolean holder is required.
+	 * The default value will be false.
+	 */
+	public ToggleButtonModelAdapter(PropertyValueModel booleanHolder) {
+		this(booleanHolder, false);
+	}
+
+
+	// ********** initialization **********
+
+	protected void initialize() {
+		this.booleanChangeListener = this.buildBooleanChangeListener();
+	}
+
+	protected PropertyChangeListener buildBooleanChangeListener() {
+		return new PropertyChangeListener() {
+			public void propertyChanged(PropertyChangeEvent e) {
+				ToggleButtonModelAdapter.this.booleanChanged(e);
+			}
+		    @Override
+			public String toString() {
+				return "boolean listener";
+			}
+		};
+	}
+
+
+	// ********** ButtonModel implementation **********
+
+	/**
+	 * Extend to update the underlying model if necessary.
+	 */
+    @Override
+	public void setSelected(boolean b) {
+		if (this.isSelected() != b) {	// stop the recursion!
+			super.setSelected(b);//put the super call first, otherwise the following gets called twice
+			this.booleanHolder.setValue(Boolean.valueOf(b));
+		}
+	}
+
+	/**
+	 * Extend to start listening to the underlying model if necessary.
+	 */
+    @Override
+	public void addActionListener(ActionListener l) {
+		if (this.hasNoListeners()) {
+			this.engageModel();
+		}
+		super.addActionListener(l);
+	}
+
+	/**
+	 * Extend to stop listening to the underlying model if appropriate.
+	 */
+    @Override
+	public void removeActionListener(ActionListener l) {
+		super.removeActionListener(l);
+		if (this.hasNoListeners()) {
+			this.disengageModel();
+		}
+	}
+
+	/**
+	 * Extend to start listening to the underlying model if necessary.
+	 */
+    @Override
+	public void addItemListener(ItemListener l) {
+		if (this.hasNoListeners()) {
+			this.engageModel();
+		}
+		super.addItemListener(l);
+	}
+
+	/**
+	 * Extend to stop listening to the underlying model if appropriate.
+	 */
+    @Override
+	public void removeItemListener(ItemListener l) {
+		super.removeItemListener(l);
+		if (this.hasNoListeners()) {
+			this.disengageModel();
+		}
+	}
+
+	/**
+	 * Extend to start listening to the underlying model if necessary.
+	 */
+    @Override
+	public void addChangeListener(ChangeListener l) {
+		if (this.hasNoListeners()) {
+			this.engageModel();
+		}
+		super.addChangeListener(l);
+	}
+
+	/**
+	 * Extend to stop listening to the underlying model if appropriate.
+	 */
+    @Override
+	public void removeChangeListener(ChangeListener l) {
+		super.removeChangeListener(l);
+		if (this.hasNoListeners()) {
+			this.disengageModel();
+		}
+	}
+
+
+	// ********** queries **********
+
+	/**
+	 * Return whether we have no listeners at all.
+	 */
+	protected boolean hasNoListeners() {
+		return this.listenerList.getListenerCount() == 0;
+	}
+
+	protected boolean getDefaultValue() {
+		return this.defaultValue;
+	}
+
+
+	// ********** behavior **********
+
+	/**
+	 * Synchronize with the specified value.
+	 * If it is null, use the default value (which is typically false).
+	 */
+	protected void setSelected(Boolean value) {
+		if (value == null) {
+			this.setSelected(this.getDefaultValue());
+		} else {
+			this.setSelected(value.booleanValue());
+		}
+	}
+
+	/**
+	 * The underlying model has changed - synchronize accordingly.
+	 */
+	protected void booleanChanged(PropertyChangeEvent e) {
+		this.setSelected((Boolean) e.newValue());
+	}
+
+	protected void engageModel() {
+		this.booleanHolder.addPropertyChangeListener(ValueModel.VALUE, this.booleanChangeListener);
+		this.setSelected((Boolean) this.booleanHolder.getValue());
+	}
+
+	protected void disengageModel() {
+		this.booleanHolder.removePropertyChangeListener(ValueModel.VALUE, this.booleanChangeListener);
+	}
+
+
+	// ********** standard methods **********
+
+    @Override
+	public String toString() {
+		return StringTools.buildToStringFor(this, this.booleanHolder);
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/TreeModelAdapter.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/TreeModelAdapter.java
new file mode 100644
index 0000000..4d88b74
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/model/value/swing/TreeModelAdapter.java
@@ -0,0 +1,700 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.model.value.swing;
+
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.swing.event.TreeModelListener;
+import javax.swing.tree.TreePath;
+
+import org.eclipse.jpt.utility.internal.StringTools;
+import org.eclipse.jpt.utility.internal.model.event.ListChangeEvent;
+import org.eclipse.jpt.utility.internal.model.event.PropertyChangeEvent;
+import org.eclipse.jpt.utility.internal.model.event.StateChangeEvent;
+import org.eclipse.jpt.utility.internal.model.listener.ListChangeListener;
+import org.eclipse.jpt.utility.internal.model.listener.PropertyChangeListener;
+import org.eclipse.jpt.utility.internal.model.listener.StateChangeListener;
+import org.eclipse.jpt.utility.internal.model.value.ListValueModel;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ReadOnlyPropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.TreeNodeValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+
+/**
+ * This javax.swing.tree.TreeModel can be used to keep a TreeModelListener
+ * (e.g. a JTree) in synch with a tree of TreeNodeValueModel objects. Unlike
+ * javax.swing.tree.DefaultTreeModel, you do not add and remove nodes with
+ * methods implemented here. You can add and remove nodes by adding and
+ * removing them directly to/from the nodes (or, more typically, the domain
+ * objects the nodes are wrapping and listening to).
+ * 
+ * Due to limitations in JTree, the root of the tree can never be null,
+ * which, typically, should not be a problem. (If you want to display an empty
+ * tree you can set the JTree's treeModel to null.)
+ */
+public class TreeModelAdapter extends AbstractTreeModel {
+
+	/**
+	 * A value model on the underlying tree's root node and its
+	 * corresponding listener. This allows clients to swap out
+	 * the entire tree. Due to limitations in JTree, the root should
+	 * never be set to null while we have listeners.
+	 */
+	private PropertyValueModel rootHolder;
+	private PropertyChangeListener rootListener;
+
+	/**
+	 * A listener that notifies us when a node's internal
+	 * "state" changes (as opposed to the node's value or list of
+	 * children), allowing us to forward notification to our listeners.
+	 */
+	private StateChangeListener nodeStateListener;
+
+	/**
+	 * A listener that notifies us when a node's "value"
+	 * changes (as opposed to the node's state or list of
+	 * children), allowing us to forward notification to our listeners.
+	 * Typically, this will only happen with nodes that hold
+	 * primitive data.
+	 */
+	private PropertyChangeListener nodeValueListener;
+
+	/**
+	 * A listener that notifies us when an underlying node's
+	 * "list" of children changes, allowing us to keep our
+	 * internal tree in synch with the underlying tree model.
+	 */
+	private ListChangeListener childrenListener;
+
+	/* these attributes make up our internal tree */
+	/**
+	 * The root cannot be null while we have listeners, which is
+	 * most of the time. The root is cached so we can disengage
+	 * from it when it has been swapped out.
+	 */
+	private TreeNodeValueModel root;
+
+	/**
+	 * Map the nodes to their lists of children.
+	 * We cache these so we can swap out the entire list of children
+	 * when we receive a #listChanged() event (which does not include
+	 * the items that were affected).
+	 * @see EventChangePolicy#rebuildChildren()
+	 */
+	IdentityHashMap<TreeNodeValueModel, List<TreeNodeValueModel>> childrenLists;
+
+	/**
+	 * Map the children models to their parents.
+	 * We cache these so we can figure out the "real" source of the
+	 * list change events (the parent).
+	 * @see EventChangePolicy#parent()
+	 */
+	IdentityHashMap<ListValueModel, TreeNodeValueModel> parents;
+
+
+	// ********** constructors **********
+
+	/**
+	 * Construct a tree model for the specified root.
+	 */
+	public TreeModelAdapter(PropertyValueModel rootHolder) {
+		super();
+		if (rootHolder == null) {
+			throw new NullPointerException();
+		}
+		this.rootHolder = rootHolder;
+	}
+
+	/**
+	 * Construct a tree model for the specified root.
+	 */
+	public TreeModelAdapter(TreeNodeValueModel root) {
+		this(new ReadOnlyPropertyValueModel(root));
+	}
+
+
+	// ********** initialization **********
+
+	@Override
+	protected void initialize() {
+		super.initialize();
+		this.rootListener = this.buildRootListener();
+		this.nodeStateListener = this.buildNodeStateListener();
+		this.nodeValueListener = this.buildNodeValueListener();
+		this.childrenListener = this.buildChildrenListener();
+		this.childrenLists = new IdentityHashMap<TreeNodeValueModel, List<TreeNodeValueModel>>();
+		this.parents = new IdentityHashMap<ListValueModel, TreeNodeValueModel>();
+	}
+
+	private PropertyChangeListener buildRootListener() {
+		return new PropertyChangeListener() {
+			public void propertyChanged(PropertyChangeEvent e) {
+				TreeModelAdapter.this.rootChanged();
+			}
+			@Override
+			public String toString() {
+				return "root listener";
+			}
+		};
+	}
+
+	private PropertyChangeListener buildNodeValueListener() {
+		return new PropertyChangeListener() {
+			public void propertyChanged(PropertyChangeEvent e) {
+				TreeModelAdapter.this.nodeChanged((TreeNodeValueModel) e.getSource());
+			}
+			@Override
+			public String toString() {
+				return "node value listener";
+			}
+		};
+	}
+
+	private StateChangeListener buildNodeStateListener() {
+		return new StateChangeListener() {
+			public void stateChanged(StateChangeEvent e) {
+				TreeModelAdapter.this.nodeChanged((TreeNodeValueModel) e.getSource());
+			}
+			@Override
+			public String toString() {
+				return "node state listener";
+			}
+		};
+	}
+
+	private ListChangeListener buildChildrenListener() {
+		return new ListChangeListener() {
+			public void itemsAdded(ListChangeEvent e) {
+				new EventChangePolicy(e).addChildren();
+			}
+			public void itemsRemoved(ListChangeEvent e) {
+				new EventChangePolicy(e).removeChildren();
+			}
+			public void itemsReplaced(ListChangeEvent e) {
+				new EventChangePolicy(e).replaceChildren();
+			}
+			public void itemsMoved(ListChangeEvent e) {
+				new EventChangePolicy(e).moveChildren();
+			}
+			public void listCleared(ListChangeEvent e) {
+				new EventChangePolicy(e).clearChildren();
+			}
+			public void listChanged(ListChangeEvent e) {
+				new EventChangePolicy(e).rebuildChildren();
+			}
+			@Override
+			public String toString() {
+				return "children listener";
+			}
+		};
+	}
+
+
+	// ********** TreeModel implementation **********
+
+	public Object getRoot() {
+		return this.root;
+	}
+
+	public Object getChild(Object parent, int index) {
+		return ((TreeNodeValueModel) parent).getChild(index);
+	}
+
+	public int getChildCount(Object parent) {
+		return ((TreeNodeValueModel) parent).childrenSize();
+	}
+
+	public boolean isLeaf(Object node) {
+		return ((TreeNodeValueModel) node).isLeaf();
+	}
+
+	public void valueForPathChanged(TreePath path, Object newValue) {
+		((TreeNodeValueModel) path.getLastPathComponent()).setValue(newValue);
+	}
+
+	public int getIndexOfChild(Object parent, Object child) {
+		return ((TreeNodeValueModel) parent).indexOfChild((TreeNodeValueModel) child);
+	}
+
+	/**
+	 * Extend to start listening to the underlying model if necessary.
+	 */
+    @Override
+	public void addTreeModelListener(TreeModelListener l) {
+		if (this.hasNoTreeModelListeners()) {
+			this.engageModel();
+		}
+		super.addTreeModelListener(l);
+	}
+
+	/**
+	 * Extend to stop listening to the underlying model if appropriate.
+	 */
+    @Override
+	public void removeTreeModelListener(TreeModelListener l) {
+		super.removeTreeModelListener(l);
+		if (this.hasNoTreeModelListeners()) {
+			this.disengageModel();
+		}
+	}
+
+
+	// ********** behavior **********
+
+	/**
+	 * Listen to the root and all the other nodes
+	 * in the underlying tree model.
+	 */
+	private void engageModel() {
+		this.rootHolder.addPropertyChangeListener(ValueModel.VALUE, this.rootListener);
+		this.root = (TreeNodeValueModel) this.rootHolder.getValue();
+		if (this.root == null) {
+			throw new NullPointerException();	// the root cannot be null while we have listeners
+		}
+		this.engageNode(this.root);
+		this.addRoot();
+	}
+
+	/**
+	 * Add the root and all of the nodes to the underlying tree.
+	 */
+	private void addRoot() {
+		this.addNode(0, this.root);
+	}
+
+	/**
+	 * Stop listening to the root and all the other
+	 * nodes in the underlying tree model.
+	 */
+	private void disengageModel() {
+		this.removeRoot();
+		this.disengageNode(this.root);
+		this.root = null;
+		this.rootHolder.removePropertyChangeListener(ValueModel.VALUE, this.rootListener);
+	}
+
+	/**
+	 * Remove the root and all of the nodes from the underlying tree.
+	 */
+	private void removeRoot() {
+		this.removeNode(0, this.root);
+	}
+
+	/**
+	 * The root has been swapped.
+	 * This method is a bit gnarly because the API for notifying listeners
+	 * that the root has changed is a bit inconsistent with that used for
+	 * non-root nodes.
+	 */
+	void rootChanged() {
+		TreeNodeValueModel newRoot = (TreeNodeValueModel) this.rootHolder.getValue();
+		if (newRoot == null) {
+			throw new NullPointerException();	// the root cannot be null while we have listeners
+		}
+		// remove all the current root's children from the tree
+		// and remove the it from the internal tree
+		this.removeRoot(); 
+
+		// save the old root and swap in the new root
+		TreeNodeValueModel oldRoot = this.root;
+		this.root = newRoot;
+
+		// we must be listening to both the old and new roots when we fire the event
+		// because their values can be affected by whether they have listeners
+		this.engageNode(this.root);
+		this.fireTreeRootReplaced(this.root);
+		// now we can stop listening to the old root
+		this.disengageNode(oldRoot);
+
+		// add the new root to the internal tree and
+		// add all its children to the tree also
+		this.addRoot();
+	}
+
+	/**
+	 * Either the "value" or the "state" of the specified node has changed,
+	 * forward notification to our listeners.
+	 */
+	void nodeChanged(TreeNodeValueModel node) {
+		TreeNodeValueModel parent = node.getParent();
+		if (parent == null) {
+			this.fireTreeRootChanged(node);
+		} else {
+			this.fireTreeNodeChanged(parent.path(), parent.indexOfChild(node), node);
+		}
+	}
+
+	/**
+	 * Listen to the nodes, notify our listeners that the nodes were added,
+	 * and then add the nodes to our internal tree.
+	 * We must listen to the nodes before notifying anybody, because
+	 * adding a listener can change the value of a node.
+	 */
+	void addChildren(Object[] path, int[] childIndices, Object[] children) {
+		int len = childIndices.length;
+		for (int i = 0; i < len; i++) {
+			this.engageNode((TreeNodeValueModel) children[i]);
+		}
+		this.fireTreeNodesInserted(path, childIndices, children);
+		for (int i = 0; i < len; i++) {
+			this.addNode(childIndices[i], (TreeNodeValueModel) children[i]);
+		}
+	}
+
+	/**
+	 * Listen to the node and its children model.
+	 */
+	private void engageNode(TreeNodeValueModel node) {
+		node.addStateChangeListener(this.nodeStateListener);
+		node.addPropertyChangeListener(ValueModel.VALUE, this.nodeValueListener);
+		node.getChildrenModel().addListChangeListener(ValueModel.VALUE, this.childrenListener);
+	}
+
+	/**
+	 * Add the node to our internal tree;
+	 * then recurse down through the node's children,
+	 * adding them to the internal tree also.
+	 */
+	private void addNode(int index, TreeNodeValueModel node) {
+		this.addNodeToInternalTree(node.getParent(), index, node, node.getChildrenModel());
+		new NodeChangePolicy(node).addChildren();
+	}
+
+	/**
+	 * Add the specified node to our internal tree.
+	 */
+	private void addNodeToInternalTree(TreeNodeValueModel parent, int index, TreeNodeValueModel node, ListValueModel childrenModel) {
+		List<TreeNodeValueModel> siblings = this.childrenLists.get(parent);
+		if (siblings == null) {
+			siblings = new ArrayList<TreeNodeValueModel>();
+			this.childrenLists.put(parent, siblings);
+		}
+		siblings.add(index, node);
+
+		this.parents.put(childrenModel, node);
+	}
+
+	/**
+	 * Remove nodes from our internal tree, notify our listeners that the
+	 * nodes were removed, then stop listening to the nodes.
+	 * We must listen to the nodes until after notifying anybody, because
+	 * removing a listener can change the value of a node.
+	 */
+	void removeChildren(Object[] path, int[] childIndices, Object[] children) {
+		int len = childIndices.length;
+		for (int i = 0; i < len; i++) {
+			// the indices slide down a notch each time we remove a child
+			this.removeNode(childIndices[i] - i, (TreeNodeValueModel) children[i]);
+		}
+		this.fireTreeNodesRemoved(path, childIndices, children);
+		for (int i = 0; i < len; i++) {
+			this.disengageNode((TreeNodeValueModel) children[i]);
+		}
+	}
+
+	/**
+	 * First, recurse down through the node's children,
+	 * removing them from our internal tree;
+	 * then remove the node itself from our internal tree.
+	 */
+	private void removeNode(int index, TreeNodeValueModel node) {
+		new NodeChangePolicy(node).removeChildren();
+		this.removeNodeFromInternalTree(node.getParent(), index, node, node.getChildrenModel());
+	}
+
+	/**
+	 * Remove the specified node from our internal tree.
+	 */
+	private void removeNodeFromInternalTree(TreeNodeValueModel parent, int index, TreeNodeValueModel node, ListValueModel childrenModel) {
+		this.parents.remove(childrenModel);
+
+		List<TreeNodeValueModel> siblings = this.childrenLists.get(parent);
+		siblings.remove(index);
+		if (siblings.isEmpty()) {
+			this.childrenLists.remove(parent);
+		}
+	}
+
+	/**
+	 * Stop listening to the node and its children model.
+	 */
+	private void disengageNode(TreeNodeValueModel node) {
+		node.getChildrenModel().removeListChangeListener(ValueModel.VALUE, this.childrenListener);
+		node.removePropertyChangeListener(ValueModel.VALUE, this.nodeValueListener);
+		node.removeStateChangeListener(this.nodeStateListener);
+	}
+
+	void moveChildren(TreeNodeValueModel parent, int targetIndex, int sourceIndex, int length) {
+		List<TreeNodeValueModel> childrenList = this.childrenLists.get(parent);
+		ArrayList<TreeNodeValueModel> temp = new ArrayList<TreeNodeValueModel>(length);
+		for (int i = 0; i < length; i++) {
+			temp.add(childrenList.remove(sourceIndex));
+		}
+		childrenList.addAll(targetIndex, temp);
+
+		this.fireTreeStructureChanged(parent.path());
+	}
+
+
+	// ********** standard methods **********
+
+	@Override
+	public String toString() {
+		return StringTools.buildToStringFor(this, this.root);
+	}
+
+
+	// ********** inner classes **********
+
+	/**
+	 * Coalesce some of the common change policy behavior.
+	 */
+	private abstract class ChangePolicy {
+
+		ChangePolicy() {
+			super();
+		}
+
+		/**
+		 * Add the current set of children.
+		 */
+		void addChildren() {
+			TreeModelAdapter.this.addChildren(this.parent().path(), this.childIndices(), this.childArray());
+		}
+
+		/**
+		 * Remove the current set of children.
+		 */
+		void removeChildren() {
+			TreeModelAdapter.this.removeChildren(this.parent().path(), this.childIndices(), this.childArray());
+		}
+
+		/**
+		 * Return an array of the indices of the current set of children,
+		 * which should be contiguous.
+		 */
+		int[] childIndices() {
+			return this.buildIndices(this.childrenStartIndex(), this.childrenSize());
+		}
+
+		/**
+		 * Return an array of the current set of children.
+		 */
+		Object[] childArray() {
+			return this.buildArray(this.children(), this.childrenSize());
+		}
+
+		/**
+		 * Build an array to hold the elements in the specified iterator.
+		 * If they are different sizes, something is screwed up...
+		 */
+		Object[] buildArray(Iterator<?> stream, int size) {
+			Object[] array = new Object[size];
+			for (int i = 0; stream.hasNext(); i++) {
+				array[i] = stream.next();
+			}
+			return array;
+		}
+
+		/**
+		 * Return a set of indices, starting at zero and
+		 * continuing for the specified size.
+		 */
+		int[] buildIndices(int size) {
+			return buildIndices(0, size);
+		}
+
+		/**
+		 * Return a set of indices, starting at the specified index and
+		 * continuing for the specified size.
+		 */
+		int[] buildIndices(int start, int size) {
+			int[] indices = new int[size];
+			int index = start;
+			for (int i = 0; i < size; i++) {
+				indices[i] = index++;
+			}
+			return indices;
+		}
+
+		/**
+		 * Return the parent of the current set of children.
+		 */
+		abstract TreeNodeValueModel parent();
+
+		/**
+		 * Return the starting index for the current set of children.
+		 */
+		abstract int childrenStartIndex();
+
+		/**
+		 * Return the size of the current set of children.
+		 */
+		abstract int childrenSize();
+
+		/**
+		 * Return an interator on the current set of children.
+		 */
+		abstract Iterator children();
+
+	}
+
+
+	/**
+	 * Wraps a ListChangeEvent for adding, removing, replacing,
+	 * and changing children.
+	 */
+	private class EventChangePolicy extends ChangePolicy {
+		private ListChangeEvent event;
+
+		EventChangePolicy(ListChangeEvent event) {
+			this.event = event;
+		}
+
+		/**
+		 * Map the ListChangeEvent's source to the corresponding parent.
+		 */
+		@Override
+		TreeNodeValueModel parent() {
+			return TreeModelAdapter.this.parents.get(this.event.getSource());
+		}
+
+		/**
+		 * The ListChangeEvent's item index is the children start index.
+		 */
+		@Override
+		int childrenStartIndex() {
+			return this.event.index();
+		}
+
+		/**
+		 * The ListChangeEvent's size is the children size.
+		 */
+		@Override
+		int childrenSize() {
+			return this.event.itemsSize();
+		}
+
+		/**
+		 * The ListChangeEvent's items are the children.
+		 */
+		@Override
+		Iterator children() {
+			return this.event.items();
+		}
+
+		/**
+		 * Remove the old nodes and add the new ones.
+		 */
+		void replaceChildren() {
+			Object[] parentPath = this.parent().path();
+			int[] childIndices = this.childIndices();
+			TreeModelAdapter.this.removeChildren(parentPath, childIndices, this.replacedChildren());
+			TreeModelAdapter.this.addChildren(parentPath, childIndices, this.childArray());
+		}
+
+		/**
+		 * Remove the old nodes and add the new ones.
+		 */
+		void moveChildren() {
+			TreeModelAdapter.this.moveChildren(this.parent(), this.event.targetIndex(), this.event.sourceIndex(), this.event.moveLength());
+		}
+
+		/**
+		 * Clear all the nodes.
+		 */
+		void clearChildren() {
+			TreeNodeValueModel parent = this.parent();
+			Object[] parentPath = parent.path();
+			List<TreeNodeValueModel> childrenList = TreeModelAdapter.this.childrenLists.get(parent);
+			int[] childIndices = this.buildIndices(childrenList.size());
+			Object[] childArray = this.buildArray(childrenList.iterator(), childrenList.size());
+			TreeModelAdapter.this.removeChildren(parentPath, childIndices, childArray);
+		}
+
+		/**
+		 * Remove all the old nodes and add all the new nodes.
+		 */
+		void rebuildChildren() {
+			TreeNodeValueModel parent = this.parent();
+			Object[] parentPath = parent.path();
+			List<TreeNodeValueModel> childrenList = TreeModelAdapter.this.childrenLists.get(parent);
+			int[] childIndices = this.buildIndices(childrenList.size());
+			Object[] childArray = this.buildArray(childrenList.iterator(), childrenList.size());
+			TreeModelAdapter.this.removeChildren(parentPath, childIndices, childArray);
+
+			childIndices = this.buildIndices(parent.getChildrenModel().size());
+			childArray = this.buildArray((Iterator) parent.getChildrenModel().getValue(), parent.childrenSize());
+			TreeModelAdapter.this.addChildren(parentPath, childIndices, childArray);
+		}
+
+		/**
+		 * The ListChangeEvent's replaced items are the replaced children.
+		 */
+		Object[] replacedChildren() {
+			return this.buildArray(this.event.replacedItems(), this.event.itemsSize());
+		}
+
+	}
+
+
+	/**
+	 * Wraps a TreeNodeValueModel for adding and removing its children.
+	 */
+	private class NodeChangePolicy extends ChangePolicy {
+		private TreeNodeValueModel node;
+
+		NodeChangePolicy(TreeNodeValueModel node) {
+			this.node = node;
+		}
+
+		/**
+		 * The node itself is the parent.
+		 */
+		@Override
+		TreeNodeValueModel parent() {
+			return this.node;
+		}
+
+		/**
+		 * Since we will always be dealing with all of the node's
+		 * children, the children start index is always zero.
+		 */
+		@Override
+		int childrenStartIndex() {
+			return 0;
+		}
+
+		/**
+		 * Since we will always be dealing with all of the node's
+		 * children, the children size is always equal to the size
+		 * of the children model.
+		 */
+		@Override
+		int childrenSize() {
+			return this.node.getChildrenModel().size();
+		}
+
+		/**
+		 * Since we will always be dealing with all of the node's
+		 * children, the children are all the objects held by
+		 * the children model.
+		 */
+		@Override
+		Iterator children() {
+			return (Iterator) this.node.getChildrenModel().getValue();
+		}
+
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/CachingComboBoxModel.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/CachingComboBoxModel.java
new file mode 100644
index 0000000..8ebe261
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/CachingComboBoxModel.java
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.swing;
+
+import javax.swing.ComboBoxModel;
+
+/**
+ * This interface allows a client to better control the performance of
+ * a combo box model by allowing the client to specify when it is
+ * acceptable for the model to "cache" and "uncache" its list of elements.
+ * The model may ignore these hints if appropriate.
+ */
+public interface CachingComboBoxModel extends ComboBoxModel {
+    
+    /**
+     * Cache the comboBoxModel List.  If you call this, you
+     * must make sure to call uncacheList() as well.  Otherwise
+     * stale data will be in the ComboBox until cacheList() is 
+     * called again or uncacheList() is called.
+     */
+    void cacheList();
+    
+    /**
+     * Clear the cached list.  Next time the list is needed it will
+     * be built when it is not cached.
+     */
+    void uncacheList();
+
+    /**
+     * Check to see if the list is already cached.  This can be used for 
+     * MouseEvents, since they are not terribly predictable.
+     */
+    boolean isCached();
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/CheckBoxTableCellRenderer.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/CheckBoxTableCellRenderer.java
new file mode 100644
index 0000000..ae63d33
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/CheckBoxTableCellRenderer.java
@@ -0,0 +1,221 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.swing;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.BorderFactory;
+import javax.swing.Icon;
+import javax.swing.JCheckBox;
+import javax.swing.JTable;
+import javax.swing.SwingConstants;
+import javax.swing.UIManager;
+import javax.swing.border.Border;
+
+import org.eclipse.jpt.utility.internal.swing.TableCellEditorAdapter.ImmediateEditListener;
+
+/**
+ * Make the cell look like a check box.
+ */
+public class CheckBoxTableCellRenderer implements TableCellEditorAdapter.Renderer {
+
+	/** the component used to paint the cell */
+	private JCheckBox checkBox;
+	
+	/** the listener to be notified on an immediate edit */
+	protected TableCellEditorAdapter.ImmediateEditListener immediateEditListener;
+
+	/** "normal" border - assume the default table "focus" border is 1 pixel thick */
+	private static final Border NO_FOCUS_BORDER = BorderFactory.createEmptyBorder(1, 1, 1, 1);
+
+
+	// ********** constructors/initialization **********
+
+	/**
+	 * Construct a cell renderer with no label or icon.
+	 */
+	public CheckBoxTableCellRenderer() {
+		super();
+		this.initialize();
+	}
+
+	/**
+	 * Construct a cell renderer with the specified text and icon,
+	 * either of which may be null.
+	 */
+	public CheckBoxTableCellRenderer(String text, Icon icon) {
+		this();
+		this.setText(text);
+		this.setIcon(icon);
+	}
+
+	/**
+	 * Construct a cell renderer with the specified text.
+	 */
+	public CheckBoxTableCellRenderer(String text) {
+		this(text, null);
+	}
+
+	/**
+	 * Construct a cell renderer with the specified icon.
+	 */
+	public CheckBoxTableCellRenderer(Icon icon) {
+		this(null, icon);
+	}
+
+	protected void initialize() {
+		this.checkBox = this.buildCheckBox();
+		// by default, check boxes do not paint their borders
+		this.checkBox.setBorderPainted(true);
+		// this setting is recommended for check boxes inside of trees and tables
+		this.checkBox.setBorderPaintedFlat(true);
+	}
+
+	protected JCheckBox buildCheckBox() {
+		JCheckBox cb = new JCheckBox();
+		cb.addActionListener(this.buildActionListener());
+		return cb;
+	}
+	
+	private ActionListener buildActionListener() {
+		return new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				if (CheckBoxTableCellRenderer.this.immediateEditListener != null) {
+					CheckBoxTableCellRenderer.this.immediateEditListener.immediateEdit();
+				}
+			}
+		};
+	}
+
+
+	// ********** TableCellRenderer implementation **********
+
+	/**
+	 * @see javax.swing.table.TableCellRenderer#getTableCellRendererComponent(javax.swing.JTable, java.lang.Object, boolean, boolean, int, int)
+	 */
+	public Component getTableCellRendererComponent(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) {
+	    this.checkBox.setHorizontalAlignment(SwingConstants.CENTER);
+		this.checkBox.setComponentOrientation(table.getComponentOrientation());
+		this.checkBox.setFont(table.getFont());
+		this.checkBox.setEnabled(table.isEnabled());
+
+		this.checkBox.setForeground(this.foregroundColor(table, value, selected, hasFocus, row, column));
+		this.checkBox.setBackground(this.backgroundColor(table, value, selected, hasFocus, row, column));
+		// once the colors are set, calculate opaque setting
+		this.checkBox.setOpaque(this.cellIsOpaqueIn(table, value, selected, hasFocus, row, column));
+		this.checkBox.setBorder(this.border(table, value, selected, hasFocus, row, column));
+
+		this.setValue(value);
+		return this.checkBox;
+	}
+
+	/**
+	 * Return the cell's foreground color.
+	 */
+	protected Color foregroundColor(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) {
+		if (selected) {
+			if (hasFocus && table.isCellEditable(row, column)) {
+				return UIManager.getColor("Table.focusCellForeground");
+			}
+			return table.getSelectionForeground();
+		}
+		return table.getForeground();
+	}
+
+	/**
+	 * Return the cell's background color.
+	 */
+	protected Color backgroundColor(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) {
+		if (selected) {
+			if (hasFocus && table.isCellEditable(row, column)) {
+				return UIManager.getColor("Table.focusCellBackground");
+			}
+			return table.getSelectionBackground();
+		}
+		return table.getBackground();
+	}
+
+	/**
+	 * Return the cell's border.
+	 */
+	protected Border border(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) {
+		return hasFocus ?  UIManager.getBorder("Table.focusCellHighlightBorder") : NO_FOCUS_BORDER;
+	}
+
+	/**
+	 * Return whether the cell should be opaque in the table.
+	 * If the cell's background is the same as the table's background
+	 * and table is opaque, we don't need to paint the background -
+	 * the table will do it.
+	 */
+	protected boolean cellIsOpaqueIn(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) {
+		Color cellBackground = this.checkBox.getBackground();
+		Color tableBackground = table.getBackground();
+		return ! (table.isOpaque() && cellBackground.equals(tableBackground));
+	}
+
+	/**
+	 * Set the check box's value.
+	 */
+	protected void setValue(Object value) {
+		// CR#3999318 - This null check needs to be removed once JDK bug is fixed
+		if (value == null) {
+			value = Boolean.FALSE;
+		}
+		this.checkBox.setSelected(((Boolean) value).booleanValue());
+	}
+
+
+	// ********** TableCellEditorAdapter.Renderer implementation **********
+
+	/**
+	 * @see TableCellEditorAdapter
+	 */
+	public Object getValue() {
+		return Boolean.valueOf(this.checkBox.isSelected());
+	}
+	
+	/**
+	 * @see TableCellEditorAdapter
+	 */
+	public void setImmediateEditListener(ImmediateEditListener listener) {
+		this.immediateEditListener = listener;
+	}
+
+	// ********** public API **********
+
+	/**
+	 * Set the check box's text; which by default is blank.
+	 */
+	public void setText(String text) {
+		this.checkBox.setText(text);
+	}
+
+	/**
+	 * Set the check box's icon; which by default is not present.
+	 */
+	public void setIcon(Icon icon) {
+		this.checkBox.setIcon(icon);
+	}
+
+	/**
+	 * Return the renderer's preferred height. This allows you
+	 * to set the table's row height to something the check box
+	 * will look good in....
+	 */
+	public int getPreferredHeight() {
+		// add in space for the border top and bottom
+		return (int) this.checkBox.getPreferredSize().getHeight() + 2;
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/ComboBoxTableCellRenderer.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/ComboBoxTableCellRenderer.java
new file mode 100644
index 0000000..942e0b2
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/ComboBoxTableCellRenderer.java
@@ -0,0 +1,339 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.swing;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Graphics;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.BorderFactory;
+import javax.swing.ComboBoxModel;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JTable;
+import javax.swing.ListCellRenderer;
+import javax.swing.SwingConstants;
+import javax.swing.UIManager;
+import javax.swing.border.Border;
+import javax.swing.event.PopupMenuEvent;
+import javax.swing.event.PopupMenuListener;
+
+import org.eclipse.jpt.utility.internal.ClassTools;
+
+/**
+ * Make the cell look like a combo-box.
+ */
+public class ComboBoxTableCellRenderer implements TableCellEditorAdapter.Renderer {
+
+	/* caching the combo box because we are caching the comboBoxModel.
+	 * Everytime we rebuilt the comboBox we would set the model on it and not
+	 * remove the model from the old combo box.  This meant that new listeners
+	 * kept being added to the comboBoxModel for every comboBox build.
+	 * Not sure if there is a way to clear out the old combo box, or why
+	 * we were buildig a new combo box every time so I went with caching it.
+	 */
+	private JComboBox comboBox;
+	
+	/** the items used to populate the combo box */
+	private CachingComboBoxModel model;
+	private ListCellRenderer renderer;
+	Object value;
+	private static int height = -1;
+	boolean fakeFocusFlag;
+
+	/** the listener to be notified on an immediate edit */
+	protected TableCellEditorAdapter.ImmediateEditListener immediateEditListener;
+	
+	/** hold the original colors of the combo-box */
+	private static Color defaultForeground;
+	private static Color defaultBackground;
+
+	/** "normal" border - assume the default table "focus" border is 1 pixel thick */
+	private static final Border NO_FOCUS_BORDER = BorderFactory.createEmptyBorder(1, 1, 1, 1);
+
+
+	// ********** constructors/initialization **********
+
+	/**
+	 * Default constructor.
+	 */
+	private ComboBoxTableCellRenderer() {
+		super();
+		initialize();
+	}
+
+	/**
+	 * Construct a cell renderer that uses the specified combo-box model.
+	 */
+	public ComboBoxTableCellRenderer(ComboBoxModel model) {
+		this(new NonCachingComboBoxModel(model));
+	}
+	
+	/**
+	 * Construct a cell renderer that uses the specified caching combo-box model.
+	 */
+	public ComboBoxTableCellRenderer(CachingComboBoxModel model) {
+		this();
+		this.model = model;
+	}
+
+	/**
+	 * Construct a cell renderer that uses the specified
+	 * combo-box model and renderer.
+	 */
+	public ComboBoxTableCellRenderer(ComboBoxModel model, ListCellRenderer renderer) {
+		this(new NonCachingComboBoxModel(model), renderer);
+	}
+	
+	/**
+	 * Construct a cell renderer that uses the specified
+	 * caching combo-box model and renderer.
+	 */
+	public ComboBoxTableCellRenderer(CachingComboBoxModel model, ListCellRenderer renderer) {
+		this(model);
+		this.renderer = renderer;
+	}
+
+	protected void initialize() {
+		// save the original colors of the combo-box, so we
+		// can use them to paint non-selected cells
+		if (height == -1) {
+			JComboBox cb = new JComboBox();
+			cb.addItem("m");
+
+			// add in space for the border top and bottom
+			height = cb.getPreferredSize().height + 2;
+
+			defaultForeground = cb.getForeground();
+			defaultBackground = cb.getBackground();
+		}
+	}
+
+    static JLabel prototypeLabel = new JLabel("Prototype", new EmptyIcon(16), SwingConstants.LEADING);
+
+    protected JComboBox buildComboBox() {
+
+		final JComboBox result = new JComboBox() {
+			private boolean fakeFocus;
+			@Override
+			public boolean hasFocus() {
+				return fakeFocus || super.hasFocus();
+			}
+			@Override
+			public void paint(Graphics g) {
+				fakeFocus = ComboBoxTableCellRenderer.this.fakeFocusFlag;
+				super.paint(g);
+				fakeFocus = false;
+			}
+			//wrap the renderer to deal with the prototypeDisplayValue
+		    @Override
+			public void setRenderer(final ListCellRenderer aRenderer) {
+		        super.setRenderer(new ListCellRenderer(){
+		            public Component getListCellRendererComponent(JList list, Object v, int index, boolean isSelected, boolean cellHasFocus) {
+		                if (v == prototypeLabel) {
+		                    return prototypeLabel;
+		                }
+		                return aRenderer.getListCellRendererComponent(list, v, index, isSelected, cellHasFocus);
+		            }
+		        });
+		    }
+			@Override
+			public int getSelectedIndex() {
+		        boolean listNotCached = !listIsCached();
+		        if (listNotCached) {
+		            cacheList();
+		        }
+		        
+				int index = super.getSelectedIndex();
+
+		        if (listNotCached) {
+		            uncacheList();
+		        }
+				return index;
+		   }
+
+		};
+		// stole this code from javax.swing.DefaultCellEditor
+		result.putClientProperty("JComboBox.isTableCellEditor", Boolean.TRUE);
+		result.addActionListener(this.buildActionListener());
+		result.addPopupMenuListener(this.buildPopupMenuListener());
+		
+        //These are used to workaround problems with Swing trying to 
+        //determine the size of a comboBox with a large model
+        result.setPrototypeDisplayValue(prototypeLabel);
+        getListBox(result).setPrototypeCellValue(prototypeLabel);
+        
+		return result;
+	}
+	
+    
+    private JList getListBox(JComboBox result) {
+        return (JList) ClassTools.getFieldValue(result.getUI(), "listBox");
+    }
+
+	
+	private ActionListener buildActionListener() {
+		return new ActionListener() {
+			public void actionPerformed(ActionEvent e) {
+				JComboBox cb = (JComboBox) e.getSource();
+				Object selectedItem = cb.getSelectedItem();
+
+				// Only update the selected item and invoke immediateEdit() if the
+				// selected item actually changed, during the initialization of the
+				// editing, the model changes and causes this method to be invoked,
+				// it causes CR#3963675 to occur because immediateEdit() stop the
+				// editing, which is done at the wrong time
+				if (ComboBoxTableCellRenderer.this.value != selectedItem) {
+					ComboBoxTableCellRenderer.this.value = cb.getSelectedItem();
+					ComboBoxTableCellRenderer.this.immediateEdit();
+				}
+			}
+		};
+	}
+
+	void immediateEdit() {
+		if (this.immediateEditListener != null) {
+			this.immediateEditListener.immediateEdit();
+		}
+	}
+	
+	private PopupMenuListener buildPopupMenuListener() {
+		return new PopupMenuListener() {
+		
+			public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
+				if (listIsCached()) {
+					uncacheList();
+				}
+				cacheList();
+			}
+		
+			public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
+	            if (listIsCached()) {
+	                uncacheList();
+	            }
+
+			}
+		
+			public void popupMenuCanceled(PopupMenuEvent e) {
+	            if (listIsCached()) {
+	                uncacheList();
+	            }
+			}
+		};
+	}
+
+	
+	void cacheList() {
+		this.model.cacheList();
+	}
+	
+	void uncacheList() {
+		this.model.uncacheList();
+	}	
+	
+	boolean listIsCached() {
+		return this.model.isCached();
+	}
+	// ********** TableCellRenderer implementation **********
+
+	/**
+	 * @see javax.swing.table.TableCellRenderer#getTableCellRendererComponent(javax.swing.JTable, java.lang.Object, boolean, boolean, int, int)
+	 */
+	public Component getTableCellRendererComponent(JTable table, Object val, boolean selected, boolean hasFocus, int row, int column) {
+		this.fakeFocusFlag = selected || hasFocus;
+		if (this.comboBox == null) {
+			this.comboBox = this.buildComboBox();
+	
+			this.comboBox.setComponentOrientation(table.getComponentOrientation());
+			this.comboBox.setModel(this.model);
+			if (this.renderer != null) {
+				this.comboBox.setRenderer(this.renderer);
+			}
+			this.comboBox.setFont(table.getFont());
+			this.comboBox.setEnabled(table.isEnabled());
+			this.comboBox.setBorder(this.border(table, val, selected, hasFocus, row, column));
+		}
+
+		// We need to go through the model since JComboBox might prevent us from
+		// selecting the value. This can happen when the value is not contained
+		// in the model, see CR#3950044 for an example
+		this.model.setSelectedItem(val);
+
+		return this.comboBox;
+	}
+
+	/**
+	 * Return the cell's foreground color.
+	 */
+	protected Color foregroundColor(JTable table, Object val, boolean selected, boolean hasFocus, int row, int column) {
+		if (selected) {
+			if (hasFocus && table.isCellEditable(row, column)) {
+				return defaultForeground;
+			}
+			return table.getSelectionForeground();
+		}
+		return defaultForeground;
+	}
+
+	/**
+	 * Return the cell's background color.
+	 */
+	protected Color backgroundColor(JTable table, Object val, boolean selected, boolean hasFocus, int row, int column) {
+		if (selected) {
+			if (hasFocus && table.isCellEditable(row, column)) {
+				return defaultBackground;
+			}
+			return table.getSelectionBackground();
+		}
+		return defaultBackground;
+	}
+
+	/**
+	 * Return the cell's border.
+	 */
+	protected Border border(JTable table, Object val, boolean selected, boolean hasFocus, int row, int column) {
+		return hasFocus ?
+			UIManager.getBorder("Table.focusCellHighlightBorder")
+		:
+			NO_FOCUS_BORDER;
+	}
+
+
+	// ********** TableCellEditorAdapter.Renderer implementation **********
+
+	/**
+	 * @see TableCellEditorAdapter#getValue()
+	 */
+	public Object getValue() {
+		return this.value;
+	}
+	
+	/**
+	 * @see TableCellEditorAdapter#setImmediateEditListener(TableCellEditorAdapter.ImmediateEditListener)
+	 */
+	public void setImmediateEditListener(TableCellEditorAdapter.ImmediateEditListener listener) {
+		this.immediateEditListener = listener;
+	}
+
+
+	// ********** public API **********
+
+	/**
+	 * Return the renderer's preferred height. This allows you
+	 * to set the row height to something the combo-box will look good in....
+	 */
+	public int getPreferredHeight() {
+		return height;
+	}
+
+}
\ No newline at end of file
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/Displayable.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/Displayable.java
similarity index 98%
rename from jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/Displayable.java
rename to jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/Displayable.java
index fbb1775..905fb6a 100644
--- a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/Displayable.java
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/Displayable.java
@@ -7,7 +7,7 @@
  * Contributors:
  *     Oracle - initial API and implementation
  ******************************************************************************/
-package org.eclipse.jpt.utility.internal;
+package org.eclipse.jpt.utility.internal.swing;
 
 import java.text.Collator;
 import java.util.Comparator;
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/EmptyIcon.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/EmptyIcon.java
new file mode 100644
index 0000000..802251f
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/EmptyIcon.java
@@ -0,0 +1,55 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.swing;
+
+import java.awt.Component;
+import java.awt.Graphics;
+
+import javax.swing.Icon;
+
+/**
+ * Implement the Icon interface with an icon that has a size but
+ * does not paint anything on the graphics context.
+ */
+public class EmptyIcon
+	implements Icon
+{
+	private final int width;
+	private final int height;
+
+	public static final EmptyIcon NULL_INSTANCE = new EmptyIcon(0);
+
+
+	public EmptyIcon(int width, int height) {
+		super();
+		this.width = width;
+		this.height = height;
+	}
+
+	public EmptyIcon(int size) {
+		this(size, size);
+	}
+
+
+	// ********** Icon implementation **********
+
+	public void paintIcon(Component c, Graphics g, int x, int y) {
+		// don't paint anything for an empty icon
+	}
+
+	public int getIconWidth() { 
+		return this.width;
+	}
+
+	public int getIconHeight() {
+		return this.height;
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/FilteringListBrowser.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/FilteringListBrowser.java
new file mode 100644
index 0000000..7031545
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/FilteringListBrowser.java
@@ -0,0 +1,138 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.swing;
+
+import javax.swing.Icon;
+import javax.swing.JComboBox;
+import javax.swing.JOptionPane;
+import javax.swing.ListModel;
+
+/**
+ * This implementation of LongListComponent.Browser uses a
+ * JOptionPane to prompt the user for the selection. The JOPtionPane
+ * is passed a FilteringListPanel to assist the user in making
+ * a selection.
+ */
+public class FilteringListBrowser 
+	implements ListChooser.ListBrowser 
+{
+	private FilteringListPanel panel;
+
+	/**
+	 * Default constructor.
+	 */
+	public FilteringListBrowser() {
+		super();
+		this.panel = this.buildPanel();
+	}
+
+	protected FilteringListPanel buildPanel() {
+		return new LocalFilteringListPanel();
+	}
+
+	/**
+	 * Prompt the user using a JOptionPane with a filtering
+	 * list panel.
+	 */
+	public void browse(ListChooser chooser) {	
+		this.initializeCellRenderer(chooser);
+		
+		int option = 
+			JOptionPane.showOptionDialog(
+				chooser, 
+				this.message(chooser), 
+				this.title(chooser), 
+				this.optionType(chooser), 
+				this.messageType(chooser), 
+				this.icon(chooser), 
+				this.selectionValues(chooser), 
+				this.initialSelectionValue(chooser)
+		);
+		
+		if (option == JOptionPane.OK_OPTION) {
+			chooser.getModel().setSelectedItem(this.panel.getSelection());
+		}
+		
+		// clear the text field so the list box is re-filtered
+		this.panel.getTextField().setText("");
+	}
+	
+	protected void initializeCellRenderer(JComboBox comboBox) {
+		// default behavior should be to use the cell renderer from the combobox.
+		this.panel.getListBox().setCellRenderer(comboBox.getRenderer());
+	}
+
+	/**
+	 * the message can be anything - here we build a component
+	 */
+	protected Object message(JComboBox comboBox) {
+		this.panel.setCompleteList(this.convertToArray(comboBox.getModel()));
+		this.panel.setSelection(comboBox.getModel().getSelectedItem());
+		return this.panel;
+	}
+
+	protected String title(JComboBox comboBox) {
+		return null;
+	}
+
+	protected int optionType(JComboBox comboBox) {
+		return JOptionPane.OK_CANCEL_OPTION;
+	}
+
+	protected int messageType(JComboBox comboBox) {
+		return JOptionPane.QUESTION_MESSAGE;
+	}
+
+	protected Icon icon(JComboBox comboBox) {
+		return null;
+	}
+
+	protected Object[] selectionValues(JComboBox comboBox) {
+		return null;
+	}
+
+	protected Object initialSelectionValue(JComboBox comboBox) {
+		return null;
+	}
+
+	/**
+	 * Convert the list of objects in the specified list model
+	 * into an array.
+	 */
+	protected Object[] convertToArray(ListModel model) {
+		int size = model.getSize();
+		Object[] result = new Object[size];
+		for (int i = 0; i < size; i++) {
+			result[i] = model.getElementAt(i);
+		}
+		return result;
+	}
+	
+	
+	// ********** custom panel **********
+	
+	protected class LocalFilteringListPanel extends FilteringListPanel {
+	
+		protected LocalFilteringListPanel() {
+			super(new Object[0], null);
+		}
+	
+		/**
+		 * Disable the performance tweak because JOptionPane
+		 * will try open wide enough to disable the horizontal scroll bar;
+		 * and it looks a bit clumsy.
+		 * @see oracle.toplink.workbench.uitools.FilteringListPanel#prototypeCellValue()
+		 */
+		protected String prototypeCellValue() {
+			return null;
+		}
+	
+	}
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/FilteringListPanel.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/FilteringListPanel.java
new file mode 100644
index 0000000..bb36dfb
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/FilteringListPanel.java
@@ -0,0 +1,444 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.swing;
+
+import java.awt.BorderLayout;
+import java.awt.Font;
+
+import javax.swing.AbstractListModel;
+import javax.swing.BorderFactory;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextField;
+import javax.swing.ListCellRenderer;
+import javax.swing.ListModel;
+import javax.swing.ListSelectionModel;
+import javax.swing.border.Border;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+
+import org.eclipse.jpt.utility.internal.SimpleStringMatcher;
+import org.eclipse.jpt.utility.internal.StringConverter;
+import org.eclipse.jpt.utility.internal.StringMatcher;
+
+/**
+ * This panel presents an entry field and a list box of choices that
+ * allows the user to filter the entries in the list box by entering
+ * a pattern in the entry field.
+ * 
+ * By default, two wildcards are allowed in the pattern:
+ * 	'*' will match any set of zero or more characters
+ * 	'?' will match any single character
+ * 
+ * The panel consists of 4 components that can be customized:
+ * 	- 1 text field
+ * 	- 1 list box
+ * 	- 2 labels, one for each of the above
+ * 
+ * Other aspects of the panel's behavior can be changed:
+ * 	- the string converter determines how the objects in the
+ * 		list are converted to strings and compared to the pattern
+ * 		entered in the text field; by default the converter simply
+ * 		uses the result of the object's #toString() method
+ * 		(if you replace the string converter, you will probably
+ * 		want to replace the list box's cell renderer also)
+ * 	- the string matcher can also be changed if you would
+ * 		like different pattern matching behavior than that
+ * 		described above
+ * 	- you can specify the maximum size of the list - this may
+ * 		force the user to enter a pattern restrictive enough
+ * 		to result in a list smaller than the maximum size; the
+ * 		default is -1, which disables the restriction
+ * 
+ * This panel is not a typical panel, in the sense that it does not share
+ * its model with clients via value models. Instead, this panel's model
+ * is set and queried directly because it is designed to be used in a
+ * dialog that directs the user's behavior (as opposed to a "normal"
+ * window).
+ */
+public class FilteringListPanel extends JPanel {
+
+	/**
+	 * The complete list of available choices
+	 * (as opposed to the partial list held by the list box).
+	 */
+	private Object[] completeList;
+
+	/**
+	 * An adapter used to convert the objects in the list
+	 * to strings so they can be run through the matcher
+	 * and displayed in the text field.
+	 */
+	StringConverter stringConverter;
+
+	/** The text field. */
+	private JTextField textField;
+	private JLabel textFieldLabel;
+	private DocumentListener textFieldListener;
+
+	/** The list box. */
+	private JList listBox;
+	private JLabel listBoxLabel;
+
+	/** The maximum number of entries displayed in the list box. */
+	private int maxListSize;
+
+	/**
+	 * The matcher used to filter the list against
+	 * the pattern entered in the text field. By default,
+	 * this allows the two wildcard characters described in
+	 * the class comment.
+	 */
+	private StringMatcher stringMatcher;
+
+	/**
+	 * Performance tweak: We use this buffer instead of
+	 * a temporary variable during filtering so we don't have
+	 * to keep re-allocating it.
+	 */
+	private Object[] buffer;
+
+	private static final Border TEXT_FIELD_LABEL_BORDER = BorderFactory.createEmptyBorder(0, 0, 5, 0);
+	private static final Border LIST_BOX_LABEL_BORDER = BorderFactory.createEmptyBorder(5, 0, 5, 0);
+
+
+	// ********** constructors **********
+
+	/**
+	 * Construct a FilteringListPanel with the specified list of choices
+	 * and initial selection. Use the default string converter to convert the
+	 * choices and selection to strings (which simply calls #toString() on
+	 * the objects).
+	 */
+	public FilteringListPanel(Object[] completeList, Object initialSelection) {
+		this(completeList, initialSelection, StringConverter.Default.instance());
+	}
+
+	/**
+	 * Construct a FilteringListPanel with the specified list of choices
+	 * and initial selection. Use the specified string converter to convert the
+	 * choices and selection to strings.
+	 */
+	public FilteringListPanel(Object[] completeList, Object initialSelection, StringConverter stringConverter) {
+		super(new BorderLayout());
+		this.completeList = completeList;
+		this.stringConverter = stringConverter;
+		this.initialize(initialSelection);
+	}
+
+
+	// ********** initialization **********
+
+	private void initialize(Object initialSelection) {
+		this.maxListSize = this.defaultMaxListSize();
+		this.buffer = new Object[this.max()];
+
+		this.textFieldListener = this.buildTextFieldListener();
+
+		this.stringMatcher = this.buildStringMatcher();
+
+		this.initializeLayout(initialSelection);
+	}
+
+	/**
+	 * Return the current max number of entries allowed in the list box.
+	 */
+	private int max() {
+		if (this.maxListSize == -1) {
+			return this.completeList.length;
+		}
+		return Math.min(this.maxListSize, this.completeList.length);
+	}
+
+	/**
+	 * Build a listener that will listen to changes in the text field
+	 * and filter the list appropriately.
+	 */
+	private DocumentListener buildTextFieldListener() {
+		return new DocumentListener() {
+			public void insertUpdate(DocumentEvent e) {
+				FilteringListPanel.this.filterList();
+			}
+			public void changedUpdate(DocumentEvent e) {
+				FilteringListPanel.this.filterList();
+			}
+			public void removeUpdate(DocumentEvent e) {
+				FilteringListPanel.this.filterList();
+			}
+			@Override
+			public String toString() {
+				return "text field listener";
+			}
+		};
+	}
+
+	private int defaultMaxListSize() {
+		return -1;
+	}
+
+	private StringMatcher buildStringMatcher() {
+		return new SimpleStringMatcher();
+	}
+
+	private void initializeLayout(Object initialSelection) {
+		// text field
+		JPanel textFieldPanel = new JPanel(new BorderLayout());
+		this.textFieldLabel = new JLabel();
+		this.textFieldLabel.setBorder(TEXT_FIELD_LABEL_BORDER);
+		textFieldPanel.add(this.textFieldLabel, BorderLayout.NORTH);
+
+		this.textField = new JTextField();
+		this.textField.getDocument().addDocumentListener(this.textFieldListener);
+		this.textFieldLabel.setLabelFor(this.textField);
+		textFieldPanel.add(this.textField, BorderLayout.CENTER);
+
+		this.add(textFieldPanel, BorderLayout.NORTH);
+
+		// list box
+		JPanel listBoxPanel = new JPanel(new BorderLayout());
+		this.listBoxLabel = new JLabel();
+		this.listBoxLabel.setBorder(LIST_BOX_LABEL_BORDER);
+		listBoxPanel.add(this.listBoxLabel, BorderLayout.NORTH);
+
+		this.listBox = new JList();
+		this.listBox.setDoubleBuffered(true);
+		this.listBox.setModel(this.buildPartialArrayListModel(this.completeList, this.max()));
+		this.listBox.getSelectionModel().setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+		// performance tweak(?)
+		this.listBox.setPrototypeCellValue(this.prototypeCellValue());
+		this.listBox.setPrototypeCellValue(null);
+		this.listBox.setCellRenderer(this.buildDefaultCellRenderer());
+		this.listBoxLabel.setLabelFor(this.listBox);
+		// bug 2777802 - scroll bars shouldn't be on the tab sequence
+		JScrollPane listBoxScrollPane = new JScrollPane(this.listBox);
+		listBoxScrollPane.getHorizontalScrollBar().setFocusable(false);
+		listBoxScrollPane.getVerticalScrollBar().setFocusable(false);
+		listBoxPanel.add(listBoxScrollPane, BorderLayout.CENTER);
+
+		// initialize the widgets
+		this.listBox.setSelectedValue(initialSelection, true);
+		this.textField.select(0, this.textField.getText().length());
+
+		this.add(listBoxPanel, BorderLayout.CENTER);
+	}
+
+
+	// ********** public API **********
+
+	public Object getSelection() {
+		return this.listBox.getSelectedValue();
+	}
+
+	public void setSelection(Object selection) {
+		this.listBox.setSelectedValue(selection, true);
+	}
+
+	public Object[] getCompleteList() {
+		return this.completeList;
+	}
+
+	/**
+	 * rebuild the filtering buffer and re-apply the filter
+	 * to the new list
+	 */
+	public void setCompleteList(Object[] completeList) {
+		this.completeList = completeList;
+		if (this.buffer.length < this.max()) {
+			// the buffer will never shrink - might want to re-consider... -bjv
+			this.buffer = new Object[this.max()];
+		}
+		this.filterList();
+	}
+
+	public int getMaxListSize() {
+		return this.maxListSize;
+	}
+
+	public void setMaxListSize(int maxListSize) {
+		this.maxListSize = maxListSize;
+		if (this.buffer.length < this.max()) {
+			// the buffer will never shrink - might want to re-consider... -bjv
+			this.buffer = new Object[this.max()];
+		}
+		this.filterList();
+	}
+
+	public StringConverter getStringConverter() {
+		return this.stringConverter;
+	}
+
+	/**
+	 * apply the new filter to the list
+	 */
+	public void setStringConverter(StringConverter stringConverter) {
+		this.stringConverter = stringConverter;
+		this.filterList();
+	}
+
+	/**
+	 * allow client code to access the text field
+	 * (so we can set the focus)
+	 */
+	public JTextField getTextField() {
+		return this.textField;
+	}
+
+	/**
+	 * allow client code to access the text field label
+	 */
+	public JLabel getTextFieldLabel() {
+		return this.textFieldLabel;
+	}
+
+	/**
+	 * convenience method
+	 */
+	public void setTextFieldLabelText(String text) {
+		this.textFieldLabel.setText(text);
+	}
+
+	/**
+	 * allow client code to access the list box
+	 * (so we can add mouse listeners for double-clicking)
+	 */
+	public JList getListBox() {
+		return this.listBox;
+	}
+
+	/**
+	 * convenience method
+	 */
+	public void setListBoxCellRenderer(ListCellRenderer renderer) {
+		this.listBox.setCellRenderer(renderer);
+	}
+
+	/**
+	 * allow client code to access the list box label
+	 */
+	public JLabel getListBoxLabel() {
+		return this.listBoxLabel;
+	}
+
+	/**
+	 * convenience method
+	 */
+	public void setListBoxLabelText(String text) {
+		this.listBoxLabel.setText(text);
+	}
+
+	/**
+	 * convenience method
+	 */
+	public void setComponentsFont(Font font) {
+		this.textFieldLabel.setFont(font);
+		this.textField.setFont(font);
+		this.listBoxLabel.setFont(font);
+		this.listBox.setFont(font);
+	}
+
+	public StringMatcher getStringMatcher() {
+		return this.stringMatcher;
+	}
+
+	/**
+	 * re-apply the filter to the list
+	 */
+	public void setStringMatcher(StringMatcher stringMatcher) {
+		this.stringMatcher = stringMatcher;
+		this.filterList();
+	}
+
+
+	// ********** internal methods **********
+
+	/**
+	 * Allow subclasses to disable performance tweak
+	 * by returning null here.
+	 */
+	protected String prototypeCellValue() {
+		return "==========> A_STRING_THAT_IS_DEFINITELY_LONGER_THAN_EVERY_STRING_IN_THE_LIST <==========";
+	}
+
+	/**
+	 * By default, use the string converter to build the text
+	 * used by the list box's cell renderer.
+	 */
+	protected ListCellRenderer buildDefaultCellRenderer() {
+		return new SimpleListCellRenderer() {
+			@Override
+			protected String buildText(Object value) {
+				return FilteringListPanel.this.stringConverter.convertToString(value);
+			}
+		};
+	}
+
+	/**
+	 * Something has changed that requires us to filter the list.
+	 * 
+	 * This method is synchronized because a fast typist can
+	 * generate events quicker than we can filter the list. (? -bjv)
+	 */
+	synchronized void filterList() {
+		// temporarily stop listening to the list box selection, since we will
+		// be changing the selection during the filtering and don't want
+		// that to affect the text field
+		this.filterList(this.textField.getText());
+	}
+
+	/**
+	 * Filter the contents of the list box to match the
+	 * specified pattern.
+	 */
+	private void filterList(String pattern) {
+		if (pattern.length() == 0) {
+			this.listBox.setModel(this.buildPartialArrayListModel(this.completeList, this.max()));
+		} else {
+			this.stringMatcher.setPatternString(pattern);
+			int j = 0;
+			int len = this.completeList.length;
+			int max = this.max();
+			for (int i = 0; i < len; i++) {
+				if (this.stringMatcher.matches(this.stringConverter.convertToString(this.completeList[i]))) {
+					this.buffer[j++] = this.completeList[i];
+				}
+				if (j == max) {
+					break;
+				}
+			}
+			this.listBox.setModel(this.buildPartialArrayListModel(this.buffer, j));
+		}
+
+		// after filtering the list, determine the appropriate selection
+		if (this.listBox.getModel().getSize() == 0) {
+			this.listBox.getSelectionModel().clearSelection();
+		} else {
+			this.listBox.getSelectionModel().setAnchorSelectionIndex(0);
+			this.listBox.getSelectionModel().setLeadSelectionIndex(0);
+			this.listBox.ensureIndexIsVisible(0);
+		}
+	}
+
+	/**
+	 * Build a list model that wraps only a portion of the specified array.
+	 * The model will include the array entries from 0 to (size - 1).
+	 */
+	private ListModel buildPartialArrayListModel(final Object[] array, final int size) {
+		return new AbstractListModel() {
+			public int getSize() {
+				return size;
+			}
+			public Object getElementAt(int index) {
+				return array[index];
+			}
+		};
+	}
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/ListChooser.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/ListChooser.java
new file mode 100644
index 0000000..52f8c8f
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/ListChooser.java
@@ -0,0 +1,427 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.swing;
+
+import java.awt.AWTEvent;
+import java.awt.AWTException;
+import java.awt.Component;
+import java.awt.EventQueue;
+import java.awt.Point;
+import java.awt.Robot;
+import java.awt.event.KeyAdapter;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyListener;
+import java.awt.event.MouseEvent;
+
+import javax.swing.ComboBoxModel;
+import javax.swing.DefaultListCellRenderer;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.ListCellRenderer;
+import javax.swing.SwingConstants;
+import javax.swing.event.PopupMenuEvent;
+import javax.swing.event.PopupMenuListener;
+import javax.swing.plaf.basic.BasicComboBoxUI;
+
+import org.eclipse.jpt.utility.internal.ClassTools;
+
+/**
+ * This component provides a way to handle selecting an item from a
+ * list that may grow too large to be handled conveniently by a combo-box. 
+ * If the list's size is less than the designated "long" list size, 
+ * the choice list will be displayed in a normal combo-box popup; 
+ * otherwise, a dialog will be used to prompt the user to choose a selection.
+ * 
+ * To change the browse mechanism, subclasses may 
+ * 	- override the method #buildBrowser()
+ *  - override the method #browse(), in which case the method 
+ * 		#buildBrowser() may be ignored.
+ */
+public class ListChooser 
+	extends JComboBox
+{
+	
+	/** the size of a "long" list - anything smaller is a "short" list */
+	int longListSize = DEFAULT_LONG_LIST_SIZE;
+	
+	/** the default size of a "long" list, which is 20 (to match JOptionPane's behavior) */
+	public static final int DEFAULT_LONG_LIST_SIZE = 20;
+	
+	/** property change associated with long list size */
+	public static final String LONG_LIST_SIZE_PROPERTY = "longListSize";
+	
+    static JLabel prototypeLabel = new JLabel("Prototype", new EmptyIcon(17), SwingConstants.LEADING);
+
+    /** 
+	 * whether the chooser is choosable.  if a chooser is not choosable,
+	 * it only serves as a display widget.  a user may not change its 
+	 * selected value.
+	 */
+	boolean choosable = true;
+	
+	/** property change associated with choosable */
+	public static final String CHOOSABLE_PROPERTY = "choosable";
+	
+	/** the browser used to make a selection from the long list - typically via a dialog */
+	private ListBrowser browser;
+	
+    private NodeSelector nodeSelector;
+    
+	/** INTERNAL - The popup is being shown.  Used to prevent infinite loop. */
+	boolean popupAlreadyInProgress;
+	
+	
+	// **************** Constructors ******************************************
+	
+	/**
+	 * Construct a list chooser for the specified model.
+	 */
+	public ListChooser(ComboBoxModel model) {
+		this(model, new NodeSelector.DefaultNodeSelector());
+	}
+	
+    public ListChooser(CachingComboBoxModel model) {
+        this(model, new NodeSelector.DefaultNodeSelector());
+    }
+    
+	public ListChooser(ComboBoxModel model, NodeSelector nodeSelector) {
+        this(new NonCachingComboBoxModel(model), nodeSelector);
+    }
+    
+    public ListChooser(CachingComboBoxModel model, NodeSelector nodeSelector) {
+        super(model);
+        this.initialize();
+        this.nodeSelector = nodeSelector;
+    }
+	// **************** Initialization ****************************************
+	
+	protected void initialize() {
+		this.addPopupMenuListener(this.buildPopupMenuListener());
+		this.setRenderer(new DefaultListCellRenderer());
+        this.addKeyListener(buildF3KeyListener());
+        
+        //These are used to workaround problems with Swing trying to 
+        //determine the size of a comboBox with a large model
+        setPrototypeDisplayValue(prototypeLabel);
+        getListBox().setPrototypeCellValue(prototypeLabel);
+	}
+	
+    
+    private JList getListBox() {
+        return (JList) ClassTools.getFieldValue(this.ui, "listBox");
+    }
+    
+	/** 
+	 * When the popup is about to be shown, the event is consumed, and 
+	 * PopupHandler determines whether to reshow the popup or to show
+	 * the long list browser.
+	 */
+	private PopupMenuListener buildPopupMenuListener() {
+		return new PopupMenuListener() {
+			public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
+				ListChooser.this.aboutToShowPopup();
+			}
+			public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
+				// do nothing
+			}
+			public void popupMenuCanceled(PopupMenuEvent e) {
+				// do nothing
+			}
+			@Override
+			public String toString() {
+				return "pop-up menu listener";
+			}
+		};
+	}
+	
+	/**
+	 * If this code is being reached due to the PopupHandler already being in progress,
+	 * then do nothing.  Otherwise, set the flag to true and launch the PopupHandler.
+	 */
+	void aboutToShowPopup() {
+		if (this.popupAlreadyInProgress) {
+			return;
+		}
+		
+		this.popupAlreadyInProgress = true;
+		EventQueue.invokeLater(new PopupHandler());
+	}
+ 
+    
+	private KeyListener buildF3KeyListener() {
+        return new KeyAdapter() {
+            @Override
+			public void keyPressed(KeyEvent e) {
+                if (e.getKeyCode() == KeyEvent.VK_F3) {
+                    goToSelectedItem();
+                }                
+            }
+			@Override
+			public String toString() {
+				return "F3 key listener";
+			}
+        };
+    }
+    
+    public void goToSelectedItem() {
+        if (getSelectedItem() != null) {
+            ListChooser.this.nodeSelector.selectNodeFor(getSelectedItem());
+        }
+    }
+    
+	// **************** Browsing **********************************************
+	
+	/** 
+	 * Lazily initialize because subclasses may have further initialization to do
+	 * before browser can be built.
+	 */
+	protected void browse() {
+		if (this.browser == null) {
+			this.browser = this.buildBrowser();
+		}
+		
+		this.browser.browse(this);
+	}
+	
+	/**
+	 * Return the "browser" used to make a selection from the long list,
+	 * typically via a dialog.
+	 */
+	protected ListChooser.ListBrowser buildBrowser() {
+		return new SimpleListBrowser();
+	}
+	
+	
+	// **************** Choosable functionality *******************************
+	
+	/** override behavior - consume selection if chooser is not choosable */
+	@Override
+	public void setSelectedIndex(int anIndex) {
+		if (this.choosable) {
+			super.setSelectedIndex(anIndex);
+		}
+	}
+	
+	private void updateArrowButton() {
+		try {
+			BasicComboBoxUI comboBoxUi = (BasicComboBoxUI) ListChooser.this.getUI();
+			JButton arrowButton = (JButton) ClassTools.getFieldValue(comboBoxUi, "arrowButton");
+			arrowButton.setEnabled(this.isEnabled() && this.choosable);
+		}
+		catch (Exception e) {
+			// this is a huge hack to try and make the combo box look right,
+			// so if it doesn't work, just swallow the exception
+		}
+	}
+	
+	
+    // **************** List Caching *******************************
+
+    void cacheList() {
+        ((CachingComboBoxModel) getModel()).cacheList();
+    }
+    
+    void uncacheList() {
+        ((CachingComboBoxModel) getModel()).uncacheList();
+    }
+
+    boolean listIsCached() {
+        return ((CachingComboBoxModel) getModel()).isCached();
+    }
+    
+	// **************** Public ************************************************
+	
+	public int getLongListSize() {
+		return this.longListSize;
+	}
+	
+	public void setLongListSize(int newLongListSize) {
+		int oldLongListSize = this.longListSize;
+		this.longListSize = newLongListSize;
+		this.firePropertyChange(LONG_LIST_SIZE_PROPERTY, oldLongListSize, newLongListSize);
+	}
+	
+	public boolean isChoosable() {
+		return this.choosable;
+	}
+	
+	public void setChoosable(boolean newValue) {
+		boolean oldValue = this.choosable;
+		this.choosable = newValue;
+		this.firePropertyChange(CHOOSABLE_PROPERTY, oldValue, newValue);
+		this.updateArrowButton();
+	}
+	
+	// **************** Handle selecting null as a value **********************
+
+	private boolean selectedIndexIsNoneSelectedItem(int index) {
+		return index == -1 &&
+				 getModel().getSize() > 0 &&
+				 getModel().getElementAt(0) == null;
+	}
+
+	@Override
+	public int getSelectedIndex() {
+        boolean listNotCached = !listIsCached();
+        if (listNotCached) {
+            cacheList();
+        }
+        
+		int index = super.getSelectedIndex();
+
+		// Use index 0 to show the <none selected> item since the actual value is
+		// null and JComboBox does not handle null values
+		if (selectedIndexIsNoneSelectedItem(index)) {
+			index = 0;
+        }
+
+        if (listNotCached) {
+            uncacheList();
+        }
+		return index;
+   }
+	
+	//wrap the renderer to deal with the prototypeDisplayValue
+    @Override
+	public void setRenderer(final ListCellRenderer aRenderer) {
+        super.setRenderer(new ListCellRenderer(){
+            public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
+                if (value == prototypeLabel) {
+                    return prototypeLabel;
+                }
+                return aRenderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+            }
+        });
+    }
+    
+    
+	// **************** Member classes ****************************************
+	
+	/**
+	 * Define the API required by this ListChooser when it must
+	 * prompt the user to select an item from the "long" list.
+	 */
+	public interface ListBrowser 
+	{
+		/**
+		 * Prompt the user to make a selection from the specified
+		 * combo-box's model.
+		 */
+		void browse(ListChooser parentChooser);
+	}
+	
+	
+	/**
+	 * Runnable class that consumes popup window and determines whether
+	 * to reshow popup or to launch browser, based on the size of the list.
+	 */
+	private class PopupHandler
+		implements Runnable
+	{
+		/** The mouse event */
+		private MouseEvent lastMouseEvent;
+		
+		/** The component from which the last mouse event was thrown */
+		private JComponent eventComponent;
+		
+		/** The location of the component at the time the last mouse event was thrown */
+		private Point componentLocation;
+		
+		/** The location of the mouse at the time the last mouse event was thrown */
+		private Point mouseLocation;
+		
+		
+		PopupHandler() {
+			this.initialize();
+		}
+		
+		private void initialize() {
+			AWTEvent event = EventQueue.getCurrentEvent();
+			
+			if (event instanceof MouseEvent) {
+				this.lastMouseEvent = (MouseEvent) event;
+				this.eventComponent = (JComponent) this.lastMouseEvent.getSource();
+				this.componentLocation = this.eventComponent.getLocationOnScreen();
+				this.mouseLocation = this.lastMouseEvent.getPoint();
+			}
+			else {
+				this.eventComponent = null;
+				this.componentLocation = null;
+				this.mouseLocation = null;
+			}
+		}
+		
+		public void run() {
+			ListChooser.this.hidePopup();
+			
+            cacheList();
+			if (ListChooser.this.choosable == true) {
+				// If the combo box model is of sufficient length, the browser will be shown.
+				// Asking the combo box model for its size should be enough to ensure that 
+				//  its size is recalculated.
+				if (ListChooser.this.getModel().getSize() > ListChooser.this.longListSize) {
+					this.checkComboBoxButton();
+					ListChooser.this.browse();
+				}
+				else {
+					ListChooser.this.showPopup();
+					this.checkMousePosition();
+				}
+			}
+            if (listIsCached()) {
+                uncacheList();
+            }
+			
+			ListChooser.this.popupAlreadyInProgress = false;
+		}
+		
+		/** If this is not done, the button never becomes un-pressed */
+		private void checkComboBoxButton() {
+			try {
+				BasicComboBoxUI comboBoxUi = (BasicComboBoxUI) ListChooser.this.getUI();
+				JButton arrowButton = (JButton) ClassTools.getFieldValue(comboBoxUi, "arrowButton");
+				arrowButton.getModel().setPressed(false);
+			}
+			catch (Exception e) {
+				// this is a huge hack to try and make the combo box look right,
+				// so if it doesn't work, just swallow the exception
+			}
+		}
+		
+		/**
+		 * Moves the mouse back to its original position before any jiggery pokery that we've done.
+		 */
+		private void checkMousePosition() {
+			if (this.eventComponent == null) {
+				return;
+			}
+			
+			final Point newComponentLocation = this.eventComponent.getLocationOnScreen();
+			boolean componentMoved = 
+				newComponentLocation.x - this.componentLocation.x != 0
+				|| newComponentLocation.y - this.componentLocation.y != 0;
+			
+			if (componentMoved) {
+				try {
+					new Robot().mouseMove(
+						newComponentLocation.x + this.mouseLocation.x,
+						newComponentLocation.y + this.mouseLocation.y
+					);
+				}
+				catch (AWTException ex) {
+					// move failed - do nothing
+				}
+			}
+		}
+	}
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/NodeSelector.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/NodeSelector.java
new file mode 100644
index 0000000..f8b4d14
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/NodeSelector.java
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.swing;
+
+/**
+ * This will be called when the user presses F3 or chooses
+ * 'Go To' in the context menu
+ */
+public interface NodeSelector 
+{       
+    /**
+     * Select the appropriate Node in the tree or the editor panel.
+     */
+    void selectNodeFor(Object item);
+    
+    /**
+     * This NodeSelector will do nothing when selectNodeFor(Object) is called
+     */
+    class DefaultNodeSelector implements NodeSelector {
+        
+        public void selectNodeFor(Object item) {
+            //default is to do nothing
+        }
+    }
+}
\ No newline at end of file
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/NonCachingComboBoxModel.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/NonCachingComboBoxModel.java
new file mode 100644
index 0000000..ee72264
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/NonCachingComboBoxModel.java
@@ -0,0 +1,73 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.swing;
+
+import javax.swing.ComboBoxModel;
+import javax.swing.event.ListDataListener;
+
+/**
+ * This implementation of the CachingComboBoxModel interface can be used
+ * whenever there is no need for caching (i.e. the contents of the selection
+ * list can be generated with little latency). All the normal ComboBoxModel
+ * behavior is delegated to a client-supplied ComboBoxModel.
+ */
+public class NonCachingComboBoxModel implements CachingComboBoxModel {
+	private ComboBoxModel wrappedComboBoxModel;
+
+	public NonCachingComboBoxModel(ComboBoxModel wrappedComboBoxModel) {
+		this.wrappedComboBoxModel = wrappedComboBoxModel;
+	}
+
+
+	// ********** CachingComboBoxModel implementation **********
+
+	public void cacheList() {
+		//do nothing
+	}
+
+	public void uncacheList() {
+		//do nothing
+	}
+
+	public boolean isCached() {
+		return false;
+	}
+
+
+	// ********** ComboBoxModel implementation **********
+
+	public void setSelectedItem(Object anItem) {
+		this.wrappedComboBoxModel.setSelectedItem(anItem);
+	}
+
+	public Object getSelectedItem() {
+		return this.wrappedComboBoxModel.getSelectedItem();
+	}
+
+
+	// ********** ListModel implementation **********
+
+	public int getSize() {
+		return this.wrappedComboBoxModel.getSize();
+	}
+
+	public Object getElementAt(int index) {
+		return this.wrappedComboBoxModel.getElementAt(index);
+	}
+
+	public void addListDataListener(ListDataListener l) {
+		this.wrappedComboBoxModel.addListDataListener(l);
+	}
+
+	public void removeListDataListener(ListDataListener l) {
+		this.wrappedComboBoxModel.removeListDataListener(l);
+	}  
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/SimpleDisplayable.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/SimpleDisplayable.java
new file mode 100644
index 0000000..d46d8e1
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/SimpleDisplayable.java
@@ -0,0 +1,178 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.swing;
+
+import javax.swing.Icon;
+
+import org.eclipse.jpt.utility.internal.model.AbstractModel;
+
+/**
+ * This implementation of Displayable converts any Object
+ * to a Displayable. Subclass it to override #displayString() and
+ * #icon() if necessary. Change notification will be fired if the
+ * object is changed.
+ * 
+ * This can be used for Strings - the display string
+ * will simply be the String itself.
+ */
+public class SimpleDisplayable
+	extends AbstractModel
+	implements Displayable
+{
+	/** The object to be converted to a Displayable. */
+	protected Object object;
+
+
+	/**
+	 * Construct a displayable for the specified object.
+	 */
+	public SimpleDisplayable(Object object) {
+		super();
+		this.object = object;
+	}
+
+	public SimpleDisplayable(boolean b) {
+		this(Boolean.valueOf(b));
+	}
+
+	public SimpleDisplayable(char c) {
+		this(new Character(c));
+	}
+
+	public SimpleDisplayable(byte b) {
+		this(new Byte(b));
+	}
+
+	public SimpleDisplayable(short s) {
+		this(new Short(s));
+	}
+
+	public SimpleDisplayable(int i) {
+		this(new Integer(i));
+	}
+
+	public SimpleDisplayable(long l) {
+		this(new Long(l));
+	}
+
+	public SimpleDisplayable(float f) {
+		this(new Float(f));
+	}
+
+	public SimpleDisplayable(double d) {
+		this(new Double(d));
+	}
+
+
+	// ********** Displayable implementation **********
+
+	public String displayString() {
+		return this.object.toString();
+	}
+
+	public Icon icon() {
+		return null;
+	}
+
+
+	// ********** Comparable implementation **********
+
+	public int compareTo(Displayable o) {
+		return DEFAULT_COMPARATOR.compare(this, o);
+	}
+
+
+	// ********** accessors **********
+
+	public Object getObject() {
+		return this.object;
+	}
+
+	public void setObject(Object object) {
+		String oldDisplayString = this.displayString();
+		Icon oldIcon = this.icon();
+		this.object = object;
+		this.firePropertyChanged(DISPLAY_STRING_PROPERTY, oldDisplayString, this.displayString());
+		this.firePropertyChanged(ICON_PROPERTY, oldIcon, this.icon());
+	}
+
+	public boolean getBoolean() {
+		return ((Boolean) this.object).booleanValue();
+	}
+
+	public void setBoolean(boolean b) {
+		this.setObject(Boolean.valueOf(b));
+	}
+
+	public char getChar() {
+		return ((Character) this.object).charValue();
+	}
+
+	public void setChar(char c) {
+		this.setObject(new Character(c));
+	}
+
+	public byte getByte() {
+		return ((Byte) this.object).byteValue();
+	}
+
+	public void setByte(byte b) {
+		this.setObject(new Byte(b));
+	}
+
+	public short getShort() {
+		return ((Short) this.object).shortValue();
+	}
+
+	public void setShort(short s) {
+		this.setObject(new Short(s));
+	}
+
+	public int getInt() {
+		return ((Integer) this.object).intValue();
+	}
+
+	public void setInt(int i) {
+		this.setObject(new Integer(i));
+	}
+
+	public long getLong() {
+		return ((Long) this.object).longValue();
+	}
+
+	public void setLong(long l) {
+		this.setObject(new Long(l));
+	}
+
+	public float getFloat() {
+		return ((Float) this.object).floatValue();
+	}
+
+	public void setFloat(float f) {
+		this.setObject(new Float(f));
+	}
+
+	public double getDouble() {
+		return ((Double) this.object).doubleValue();
+	}
+
+	public void setDouble(double d) {
+		this.setObject(new Double(d));
+	}
+
+
+	// ********** override methods **********
+
+	@Override
+	public void toString(StringBuilder sb) {
+		sb.append(this.object);
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/SimpleListBrowser.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/SimpleListBrowser.java
new file mode 100644
index 0000000..7215c6a
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/SimpleListBrowser.java
@@ -0,0 +1,86 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.swing;
+
+import javax.swing.Icon;
+import javax.swing.JComboBox;
+import javax.swing.JOptionPane;
+import javax.swing.ListModel;
+
+/**
+ * This implementation of ListChooser.Browser uses a
+ * JOptionPane to prompt the user for the selection. Subclasses 
+ * can change the dialog's title, message, and/or icon.
+ */
+public class SimpleListBrowser
+	implements ListChooser.ListBrowser 
+{
+	/** Default constructor */
+	protected SimpleListBrowser() {
+		super();
+	}
+	
+	/**
+	 * Prompt the user using a JOptionPane.
+	 */
+	public void browse(ListChooser chooser) {
+		Object selection = 
+			JOptionPane.showInputDialog(
+				chooser, 
+				this.message(chooser), 
+				this.title(chooser), 
+				this.messageType(chooser), 
+				this.icon(chooser), 
+				this.selectionValues(chooser), 
+				this.initialSelectionValue(chooser)
+			);
+		
+		if (selection != null) {
+			chooser.getModel().setSelectedItem(selection);
+		}
+	}
+	
+	protected Object message(JComboBox comboBox) {
+		return null;
+	}
+	
+	protected String title(JComboBox comboBox) {
+		return null;
+	}
+	
+	protected int messageType(JComboBox comboBox) {
+		return JOptionPane.QUESTION_MESSAGE;
+	}
+	
+	protected Icon icon(JComboBox comboBox) {
+		return null;
+	}
+	
+	protected Object[] selectionValues(JComboBox comboBox) {
+		return this.convertToArray(comboBox.getModel());
+	}
+	
+	protected Object initialSelectionValue(JComboBox comboBox) {
+		return comboBox.getModel().getSelectedItem();
+	}
+	
+	/**
+	 * Convert the list of objects in the specified list model
+	 * into an array.
+	 */
+	protected Object[] convertToArray(ListModel model) {
+		int size = model.getSize();
+		Object[] result = new Object[size];
+		for (int i = 0; i < size; i++) {
+			result[i] = model.getElementAt(i);
+		}
+		return result;
+	}
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/SimpleListCellRenderer.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/SimpleListCellRenderer.java
new file mode 100644
index 0000000..facf5d3
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/SimpleListCellRenderer.java
@@ -0,0 +1,129 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.swing;
+
+import java.awt.Component;
+
+import javax.swing.DefaultListCellRenderer;
+import javax.swing.Icon;
+import javax.swing.JList;
+
+/**
+ * This renderer should behave the same as the DefaultListCellRenderer;
+ * but it slightly refactors the calculation of the icon and text of the list
+ * cell so that subclasses can easily override the methods that build
+ * the icon and text.
+ * 
+ * In most cases, you need only override:
+ *     #buildIcon(Object value)
+ *     #buildText(Object value)
+ */
+public class SimpleListCellRenderer
+	extends DefaultListCellRenderer
+{
+
+	/**
+	 * Construct a simple renderer.
+	 */
+	public SimpleListCellRenderer() {
+		super();
+	}
+
+	@Override
+	public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
+		// substitute null for the cell value so nothing is drawn initially...
+		super.getListCellRendererComponent(list, null, index, isSelected, cellHasFocus);
+		this.setOpaque(true);
+
+		// ...then set the icon and text manually
+		this.setIcon(this.buildIcon(list, value, index, isSelected, cellHasFocus));
+		this.setText(this.buildText(list, value, index, isSelected, cellHasFocus));
+
+		this.setToolTipText(this.buildToolTipText(list, value, index, isSelected, cellHasFocus));
+
+		// the context will be initialized only if a reader is running
+		if (this.accessibleContext != null) {
+			this.accessibleContext.setAccessibleName(this.buildAccessibleName(list, value, index, isSelected, cellHasFocus));
+		}
+
+		return this;
+	}
+
+	/**
+	 * Return the icon representation of the specified cell
+	 * value and other settings. (Even more settings are
+	 * accessible via inherited getters: hasFocus, isEnabled, etc.)
+	 */
+	protected Icon buildIcon(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
+		return this.buildIcon(value);
+	}
+
+	/**
+	 * Return the icon representation of the specified cell
+	 * value. The default is to display no icon at all unless the
+	 * value itself is an icon.
+	 */
+	protected Icon buildIcon(Object value) {
+		// replicate the default behavior
+		return (value instanceof Icon) ? (Icon) value : null;
+	}
+
+	/**
+	 * Return the textual representation of the specified cell
+	 * value and other settings. (Even more settings are
+	 * accessible via inherited getters: hasFocus, isEnabled, etc.)
+	 */
+	protected String buildText(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
+		return this.buildText(value);
+	}
+
+	/**
+	 * Return the textual representation of the specified cell
+	 * value. The default is to display the object's default string
+	 * representation (as returned by #toString()); unless the
+	 * value itself is an icon, in which case no text is displayed.
+	 */
+	protected String buildText(Object value) {
+		return (value instanceof Icon) ? "" : ((value == null) ? "" : value.toString());
+	}
+
+	/**
+	 * Return the text displayed when the cursor lingers over the specified cell.
+	 * (Even more settings are accessible via inherited getters: hasFocus, isEnabled, etc.)
+	 */
+	protected String buildToolTipText(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
+		return this.buildToolTipText(value);
+	}
+
+	/**
+	 * Return the text displayed when the cursor lingers over the specified cell.
+	 */
+	protected String buildToolTipText(Object value) {
+		return null;
+	}
+
+	/**
+	 * Return the accessible name to be given to the component used to render
+	 * the given value and other settings. (Even more settings are accessible via
+	 * inherited getters: hasFocus, isEnabled, etc.)
+	 */
+	protected String buildAccessibleName(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
+		return this.buildAccessibleName(value);
+	}
+
+	/**
+	 * Return the accessible name to be given to the component used to render
+	 * the given value.
+	 */
+	protected String buildAccessibleName(Object value) {
+		return null;
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/SpinnerTableCellRenderer.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/SpinnerTableCellRenderer.java
new file mode 100644
index 0000000..777d512
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/SpinnerTableCellRenderer.java
@@ -0,0 +1,196 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.swing;
+
+import java.awt.Color;
+import java.awt.Component;
+
+import javax.swing.BorderFactory;
+import javax.swing.JComponent;
+import javax.swing.JSpinner;
+import javax.swing.JTable;
+import javax.swing.SpinnerModel;
+import javax.swing.UIManager;
+import javax.swing.border.Border;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+/**
+ * Make the cell look like a spinner.
+ */
+public class SpinnerTableCellRenderer implements TableCellEditorAdapter.Renderer {
+
+	/** the component used to paint the cell */
+	protected JSpinner spinner;
+	
+	/** the listener to be notified on an immediate edit */
+	protected TableCellEditorAdapter.ImmediateEditListener immediateEditListener;
+	
+	
+	// ********** constructors/initialization **********
+
+	/**
+	 * Construct a cell renderer that uses the default
+	 * spinner model, which is a "number" model.
+	 */
+	public SpinnerTableCellRenderer() {
+		super();
+		this.initialize();
+	}
+
+	/**
+	 * Construct a cell renderer that uses the specified
+	 * spinner model, which will determine how the values are displayed.
+	 */
+	public SpinnerTableCellRenderer(SpinnerModel model) {
+		this();
+		this.setModel(model);
+	}
+
+	protected void initialize() {
+		this.spinner = this.buildSpinner();
+	}
+
+	protected JSpinner buildSpinner() {
+		JSpinner s = new JSpinner();
+		s.addChangeListener(this.buildChangeListener());
+		return s;
+	}
+	
+	private ChangeListener buildChangeListener() {
+		return new ChangeListener() {
+			public void stateChanged(ChangeEvent e) {
+				if (SpinnerTableCellRenderer.this.immediateEditListener != null) {
+					SpinnerTableCellRenderer.this.immediateEditListener.immediateEdit();
+				}
+			}
+		};
+	}
+
+
+	// ********** TableCellRenderer implementation **********
+
+	/**
+	 * @see javax.swing.table.TableCellRenderer#getTableCellRendererComponent(javax.swing.JTable, java.lang.Object, boolean, boolean, int, int)
+	 */
+	public Component getTableCellRendererComponent(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) {
+		this.spinner.setComponentOrientation(table.getComponentOrientation());
+		this.spinner.setFont(table.getFont());
+		this.spinner.setEnabled(table.isEnabled());
+
+		JComponent editor = this.editor();
+		editor.setForeground(this.foregroundColor(table, value, selected, hasFocus, row, column));
+		editor.setBackground(this.backgroundColor(table, value, selected, hasFocus, row, column));
+		this.spinner.setBorder(this.border(table, value, selected, hasFocus, row, column));
+
+		this.setValue(value);
+		return this.spinner;
+	}
+
+	/**
+	 * Return the cell's foreground color.
+	 */
+	protected Color foregroundColor(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) {
+		if (selected) {
+			if (hasFocus && table.isCellEditable(row, column)) {
+				return UIManager.getColor("Table.focusCellForeground");
+			}
+			return table.getSelectionForeground();
+		}
+		return table.getForeground();
+	}
+
+	/**
+	 * Return the cell's background color.
+	 */
+	protected Color backgroundColor(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) {
+		if (selected) {
+			if (hasFocus && table.isCellEditable(row, column)) {
+				return UIManager.getColor("Table.focusCellBackground");
+			}
+			return table.getSelectionBackground();
+		}
+		return table.getBackground();
+	}
+
+	/**
+	 * Return the cell's border.
+	 */
+	protected Border border(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column) {
+		if (hasFocus) {
+			return UIManager.getBorder("Table.focusCellHighlightBorder");
+		}
+		if (selected) {
+			return BorderFactory.createLineBorder(table.getSelectionBackground(), 1);
+		}
+		return BorderFactory.createLineBorder(table.getBackground(), 1);
+	}
+
+	/**
+	 * Return the editor component whose colors should be set
+	 * by the renderer.
+	 */
+	protected JComponent editor() {
+		JComponent editor = this.spinner.getEditor();
+		if (editor instanceof JSpinner.DefaultEditor) {
+			// typically, the editor will be the default or one of its subclasses...
+			editor = ((JSpinner.DefaultEditor) editor).getTextField();
+		}
+		return editor;
+	}
+
+	/**
+	 * Set the spinner's value
+	 */
+	protected void setValue(Object value) {
+		// CR#3999318 - This null check needs to be removed once JDK bug is fixed
+		if (value == null) {
+			value = new Integer(0);
+		}
+		this.spinner.setValue(value);
+	}
+
+
+	// ********** TableCellEditorAdapter.Renderer implementation **********
+
+	/**
+	 * @see TableCellEditorAdapter#getValue()
+	 */
+	public Object getValue() {
+		return this.spinner.getValue();
+	}
+	
+	/**
+	 * @see TableCellEditorAdapter#setImmediateEditListener(TableCellEditorAdapter.ImmediateEditListener listener)
+	 */
+	public void setImmediateEditListener(TableCellEditorAdapter.ImmediateEditListener listener) {
+		this.immediateEditListener = listener;
+	}
+
+
+	// ********** public API **********
+
+	/**
+	 * Set the spinner's model.
+	 */
+	public void setModel(SpinnerModel model) {
+		this.spinner.setModel(model);
+	}
+
+	/**
+	 * Return the renderer's preferred height. This allows you
+	 * to set the row height to something the spinner will look good in....
+	 */
+	public int getPreferredHeight() {
+		// add in space for the border top and bottom
+		return (int) this.spinner.getPreferredSize().getHeight() + 2;
+	}
+
+}
diff --git a/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/TableCellEditorAdapter.java b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/TableCellEditorAdapter.java
new file mode 100644
index 0000000..3368f72
--- /dev/null
+++ b/jpa/plugins/org.eclipse.jpt.utility/src/org/eclipse/jpt/utility/internal/swing/TableCellEditorAdapter.java
@@ -0,0 +1,104 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.internal.swing;
+
+import java.awt.Component;
+
+import javax.swing.AbstractCellEditor;
+import javax.swing.JTable;
+import javax.swing.table.TableCellEditor;
+import javax.swing.table.TableCellRenderer;
+
+/**
+ * A table cell editor that wraps a table cell renderer.
+ */
+public class TableCellEditorAdapter extends AbstractCellEditor implements TableCellEditor {
+
+	/** delegate to a renderer */
+	private Renderer renderer;
+	
+	
+	// ********** constructors/initialization **********
+	
+	private TableCellEditorAdapter() {
+		super();
+	}
+	
+	/**
+	 * Construct a cell editor that behaves like the specified renderer.
+	 */
+	public TableCellEditorAdapter(Renderer renderer) {
+		this();
+		this.initialize(renderer);
+	}
+	
+	protected void initialize(Renderer r) {
+		this.renderer = r;
+		r.setImmediateEditListener(this.buildImmediateEditListener());
+	}
+	
+	private ImmediateEditListener buildImmediateEditListener() {
+		return new ImmediateEditListener() {
+			public void immediateEdit() {
+				TableCellEditorAdapter.this.stopCellEditing();
+			}
+		};
+	}
+	
+	
+	// ********** CellEditor implementation **********
+	
+	/**
+	 * @see javax.swing.CellEditor#getCellEditorValue()
+	 */
+	public Object getCellEditorValue() {
+		return this.renderer.getValue();
+	}
+	
+	
+	// ********** TableCellEditor implementation **********
+	
+	/**
+	 * @see javax.swing.table.TableCellEditor#getTableCellEditorComponent(javax.swing.JTable, java.lang.Object, boolean, int, int)
+	 */
+	public Component getTableCellEditorComponent(JTable table, Object value, boolean selected, int row, int column) {
+		return this.renderer.getTableCellRendererComponent(table, value, selected, true, row, column);
+	}
+	
+	
+	
+	// ********** Member classes **********************************************
+	
+	/**
+	 * This interface defines the methods that must be implemented by a renderer
+	 * that can be wrapped by a TableCellEditorAdapter.
+	 */
+	public interface Renderer extends TableCellRenderer {
+		
+		/**
+		 * Return the current value of the renderer.
+		 */
+		Object getValue();
+		
+		/**
+		 * Set the immediate edit listener
+		 */
+		void setImmediateEditListener(ImmediateEditListener listener);
+	}
+	
+	
+	public interface ImmediateEditListener {
+		
+		/**
+		 * Called when the renderer does an "immediate edit"
+		 */
+		void immediateEdit();
+	}
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/ItemCollectionListValueModelAdapterTests.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/ItemCollectionListValueModelAdapterTests.java
index 1886a8c..aabb467 100644
--- a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/ItemCollectionListValueModelAdapterTests.java
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/ItemCollectionListValueModelAdapterTests.java
@@ -16,7 +16,6 @@
 import javax.swing.Icon;
 
 import org.eclipse.jpt.utility.internal.Bag;
-import org.eclipse.jpt.utility.internal.Displayable;
 import org.eclipse.jpt.utility.internal.HashBag;
 import org.eclipse.jpt.utility.internal.model.AbstractModel;
 import org.eclipse.jpt.utility.internal.model.value.CollectionValueModel;
@@ -26,6 +25,7 @@
 import org.eclipse.jpt.utility.internal.model.value.SimpleListValueModel;
 import org.eclipse.jpt.utility.internal.model.value.SortedListValueModelAdapter;
 import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+import org.eclipse.jpt.utility.internal.swing.Displayable;
 import org.eclipse.jpt.utility.tests.internal.TestTools;
 
 import junit.framework.TestCase;
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/ItemListListValueModelAdapterTests.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/ItemListListValueModelAdapterTests.java
index 6aa0a15..01889c3 100644
--- a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/ItemListListValueModelAdapterTests.java
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/ItemListListValueModelAdapterTests.java
@@ -16,7 +16,6 @@
 import javax.swing.Icon;
 
 import org.eclipse.jpt.utility.internal.Bag;
-import org.eclipse.jpt.utility.internal.Displayable;
 import org.eclipse.jpt.utility.internal.HashBag;
 import org.eclipse.jpt.utility.internal.model.AbstractModel;
 import org.eclipse.jpt.utility.internal.model.value.CollectionValueModel;
@@ -26,6 +25,7 @@
 import org.eclipse.jpt.utility.internal.model.value.SimpleListValueModel;
 import org.eclipse.jpt.utility.internal.model.value.SortedListValueModelAdapter;
 import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+import org.eclipse.jpt.utility.internal.swing.Displayable;
 import org.eclipse.jpt.utility.tests.internal.TestTools;
 
 import junit.framework.TestCase;
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/ItemPropertyListValueModelAdapterTests.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/ItemPropertyListValueModelAdapterTests.java
index e7429dc..98372de 100644
--- a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/ItemPropertyListValueModelAdapterTests.java
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/ItemPropertyListValueModelAdapterTests.java
@@ -20,7 +20,6 @@
 import javax.swing.Icon;
 
 import org.eclipse.jpt.utility.internal.Bag;
-import org.eclipse.jpt.utility.internal.Displayable;
 import org.eclipse.jpt.utility.internal.HashBag;
 import org.eclipse.jpt.utility.internal.model.AbstractModel;
 import org.eclipse.jpt.utility.internal.model.value.ItemPropertyListValueModelAdapter;
@@ -29,6 +28,7 @@
 import org.eclipse.jpt.utility.internal.model.value.SimpleListValueModel;
 import org.eclipse.jpt.utility.internal.model.value.SortedListValueModelAdapter;
 import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+import org.eclipse.jpt.utility.internal.swing.Displayable;
 import org.eclipse.jpt.utility.tests.internal.TestTools;
 
 import junit.framework.TestCase;
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/JptUtilityModelValueTests.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/JptUtilityModelValueTests.java
index 0aa607d..eae9934 100644
--- a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/JptUtilityModelValueTests.java
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/JptUtilityModelValueTests.java
@@ -10,7 +10,7 @@
 package org.eclipse.jpt.utility.tests.internal.model.value;
 
 import org.eclipse.jpt.utility.tests.internal.model.value.prefs.JptUtilityModelValuePrefsTests;
-//import org.eclipse.jpt.utility.tests.internal.model.value.swing.JptUtilityModelValueSwingTests;
+import org.eclipse.jpt.utility.tests.internal.model.value.swing.JptUtilityModelValueSwingTests;
 
 import junit.framework.Test;
 import junit.framework.TestSuite;
@@ -21,7 +21,7 @@
 		TestSuite suite = new TestSuite(JptUtilityModelValueTests.class.getPackage().getName());
 
 		suite.addTest(JptUtilityModelValuePrefsTests.suite());
-//		suite.addTest(JptUtilityModelValueSwingTests.suite());
+		suite.addTest(JptUtilityModelValueSwingTests.suite());
 		
 		suite.addTestSuite(BufferedPropertyValueModelTests.class);
 		suite.addTestSuite(CollectionAspectAdapterTests.class);
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/CheckBoxModelAdapterTests.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/CheckBoxModelAdapterTests.java
new file mode 100644
index 0000000..78fe762
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/CheckBoxModelAdapterTests.java
@@ -0,0 +1,130 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import javax.swing.ButtonModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.EventListenerList;
+
+import org.eclipse.jpt.utility.internal.ClassTools;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimplePropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+import org.eclipse.jpt.utility.internal.model.value.swing.CheckBoxModelAdapter;
+import org.eclipse.jpt.utility.tests.internal.TestTools;
+
+import junit.framework.TestCase;
+
+public class CheckBoxModelAdapterTests extends TestCase {
+	private PropertyValueModel booleanHolder;
+	private ButtonModel buttonModelAdapter;
+	boolean eventFired;
+
+	public CheckBoxModelAdapterTests(String name) {
+		super(name);
+	}
+
+	@Override
+	protected void setUp() throws Exception {
+		super.setUp();
+		this.booleanHolder = new SimplePropertyValueModel(Boolean.TRUE);
+		this.buttonModelAdapter = new CheckBoxModelAdapter(this.booleanHolder);
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		TestTools.clear(this);
+		super.tearDown();
+	}
+
+	public void testSetSelected() throws Exception {
+		this.eventFired = false;
+		this.buttonModelAdapter.addChangeListener(new TestChangeListener() {
+			@Override
+			public void stateChanged(ChangeEvent e) {
+				CheckBoxModelAdapterTests.this.eventFired = true;
+			}
+		});
+		this.buttonModelAdapter.setSelected(false);
+		assertTrue(this.eventFired);
+		assertEquals(Boolean.FALSE, this.booleanHolder.getValue());
+	}
+
+	public void testSetValue() throws Exception {
+		this.eventFired = false;
+		this.buttonModelAdapter.addChangeListener(new TestChangeListener() {
+			@Override
+			public void stateChanged(ChangeEvent e) {
+				CheckBoxModelAdapterTests.this.eventFired = true;
+			}
+		});
+		assertTrue(this.buttonModelAdapter.isSelected());
+		this.booleanHolder.setValue(Boolean.FALSE);
+		assertTrue(this.eventFired);
+		assertFalse(this.buttonModelAdapter.isSelected());
+	}
+
+	public void testDefaultValue() throws Exception {
+		this.eventFired = false;
+		this.buttonModelAdapter.addChangeListener(new TestChangeListener() {
+			@Override
+			public void stateChanged(ChangeEvent e) {
+				CheckBoxModelAdapterTests.this.eventFired = true;
+			}
+		});
+		assertTrue(this.buttonModelAdapter.isSelected());
+		this.booleanHolder.setValue(null);
+		assertTrue(this.eventFired);
+		assertFalse(this.buttonModelAdapter.isSelected());
+
+		this.eventFired = false;
+		this.booleanHolder.setValue(Boolean.FALSE);
+		assertFalse(this.eventFired);
+		assertFalse(this.buttonModelAdapter.isSelected());
+	}
+
+	public void testHasListeners() throws Exception {
+		SimplePropertyValueModel localBooleanHolder = (SimplePropertyValueModel) this.booleanHolder;
+		assertFalse(localBooleanHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasNoListeners(this.buttonModelAdapter);
+
+		ChangeListener listener = new TestChangeListener();
+		this.buttonModelAdapter.addChangeListener(listener);
+		assertTrue(localBooleanHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasListeners(this.buttonModelAdapter);
+
+		this.buttonModelAdapter.removeChangeListener(listener);
+		assertFalse(localBooleanHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasNoListeners(this.buttonModelAdapter);
+	}
+
+	private void verifyHasNoListeners(Object model) throws Exception {
+		EventListenerList listenerList = (EventListenerList) ClassTools.getFieldValue(model, "listenerList");
+		assertEquals(0, listenerList.getListenerList().length);
+	}
+
+	private void verifyHasListeners(Object model) throws Exception {
+		EventListenerList listenerList = (EventListenerList) ClassTools.getFieldValue(model, "listenerList");
+		assertFalse(listenerList.getListenerList().length == 0);
+	}
+
+
+	// ********** member class **********
+	private class TestChangeListener implements ChangeListener {
+		TestChangeListener() {
+			super();
+		}
+		public void stateChanged(ChangeEvent e) {
+			fail("unexpected event");
+		}
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/CheckBoxModelAdapterUITest.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/CheckBoxModelAdapterUITest.java
new file mode 100644
index 0000000..2ffe323
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/CheckBoxModelAdapterUITest.java
@@ -0,0 +1,316 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.ButtonModel;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.WindowConstants;
+
+import org.eclipse.jpt.utility.internal.model.AbstractModel;
+import org.eclipse.jpt.utility.internal.model.value.PropertyAspectAdapter;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimplePropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+import org.eclipse.jpt.utility.internal.model.value.swing.CheckBoxModelAdapter;
+
+
+/**
+ * Play around with a set of check boxes.
+ */
+public class CheckBoxModelAdapterUITest {
+
+	private TestModel testModel;
+	private PropertyValueModel testModelHolder;
+	private PropertyValueModel flag1Holder;
+	private PropertyValueModel flag2Holder;
+	private PropertyValueModel notFlag2Holder;
+	private ButtonModel flag1ButtonModel;
+	private ButtonModel flag2ButtonModel;
+	private ButtonModel notFlag2ButtonModel;
+
+	public static void main(String[] args) throws Exception {
+		new CheckBoxModelAdapterUITest().exec(args);
+	}
+
+	private CheckBoxModelAdapterUITest() {
+		super();
+	}
+
+	private void exec(String[] args) throws Exception {
+		this.testModel = new TestModel(true, true);
+		this.testModelHolder = new SimplePropertyValueModel(this.testModel);
+		this.flag1Holder = this.buildFlag1Holder(this.testModelHolder);
+		this.flag1ButtonModel = this.buildCheckBoxModelAdapter(this.flag1Holder);
+		this.flag2Holder = this.buildFlag2Holder(this.testModelHolder);
+		this.flag2ButtonModel = this.buildCheckBoxModelAdapter(this.flag2Holder);
+		this.notFlag2Holder = this.buildNotFlag2Holder(this.testModelHolder);
+		this.notFlag2ButtonModel = this.buildCheckBoxModelAdapter(this.notFlag2Holder);
+		this.openWindow();
+	}
+
+	private PropertyValueModel buildFlag1Holder(ValueModel vm) {
+		return new PropertyAspectAdapter(vm, TestModel.FLAG1_PROPERTY) {
+			@Override
+			protected Object getValueFromSubject() {
+				return Boolean.valueOf(((TestModel) this.subject).isFlag1());
+			}
+			@Override
+			protected void setValueOnSubject(Object value) {
+				((TestModel) this.subject).setFlag1(((Boolean) value).booleanValue());
+			}
+		};
+	}
+
+	private PropertyValueModel buildFlag2Holder(ValueModel vm) {
+		return new PropertyAspectAdapter(vm, TestModel.FLAG2_PROPERTY) {
+			@Override
+			protected Object getValueFromSubject() {
+				return Boolean.valueOf(((TestModel) this.subject).isFlag2());
+			}
+			@Override
+			protected void setValueOnSubject(Object value) {
+				((TestModel) this.subject).setFlag2(((Boolean) value).booleanValue());
+			}
+		};
+	}
+
+	private PropertyValueModel buildNotFlag2Holder(ValueModel vm) {
+		return new PropertyAspectAdapter(vm, TestModel.NOT_FLAG2_PROPERTY) {
+			@Override
+			protected Object getValueFromSubject() {
+				return Boolean.valueOf(((TestModel) this.subject).isNotFlag2());
+			}
+			@Override
+			protected void setValueOnSubject(Object value) {
+				((TestModel) this.subject).setNotFlag2(((Boolean) value).booleanValue());
+			}
+		};
+	}
+
+	private ButtonModel buildCheckBoxModelAdapter(PropertyValueModel booleanHolder) {
+		return new CheckBoxModelAdapter(booleanHolder);
+	}
+
+	private void openWindow() {
+		JFrame window = new JFrame(this.getClass().getName());
+		window.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
+		window.addWindowListener(this.buildWindowListener());
+		window.getContentPane().add(this.buildMainPanel(), "Center");
+		window.setSize(400, 100);
+		window.setVisible(true);
+	}
+
+	private WindowListener buildWindowListener() {
+		return new WindowAdapter() {
+			@Override
+			public void windowClosing(WindowEvent e) {
+				e.getWindow().setVisible(false);
+				System.exit(0);
+			}
+		};
+	}
+
+	private Component buildMainPanel() {
+		JPanel mainPanel = new JPanel(new BorderLayout());
+		mainPanel.add(this.buildCheckBoxPanel(), BorderLayout.NORTH);
+		mainPanel.add(this.buildControlPanel(), BorderLayout.SOUTH);
+		return mainPanel;
+	}
+
+	private Component buildCheckBoxPanel() {
+		JPanel taskListPanel = new JPanel(new GridLayout(1, 0));
+		taskListPanel.add(this.buildFlag1CheckBox());
+		taskListPanel.add(this.buildFlag2CheckBox());
+		taskListPanel.add(this.buildNotFlag2CheckBox());
+		taskListPanel.add(this.buildUnattachedCheckBox());
+		return taskListPanel;
+	}
+
+	private JCheckBox buildFlag1CheckBox() {
+		JCheckBox checkBox = new JCheckBox();
+		checkBox.setText("flag 1");
+		checkBox.setModel(this.flag1ButtonModel);
+		return checkBox;
+	}
+
+	private JCheckBox buildFlag2CheckBox() {
+		JCheckBox checkBox = new JCheckBox();
+		checkBox.setText("flag 2");
+		checkBox.setModel(this.flag2ButtonModel);
+		return checkBox;
+	}
+
+	private JCheckBox buildNotFlag2CheckBox() {
+		JCheckBox checkBox = new JCheckBox();
+		checkBox.setText("not flag 2");
+		checkBox.setModel(this.notFlag2ButtonModel);
+		return checkBox;
+	}
+
+	private JCheckBox buildUnattachedCheckBox() {
+		JCheckBox checkBox = new JCheckBox("unattached");
+		checkBox.getModel().addItemListener(this.buildUnattachedItemListener());
+		return checkBox;
+	}
+
+	private ItemListener buildUnattachedItemListener() {
+		return new ItemListener() {
+			public void itemStateChanged(ItemEvent e) {
+				System.out.println("unattached state changed: " + e);
+			}
+		};
+	}
+
+	private Component buildControlPanel() {
+		JPanel controlPanel = new JPanel(new GridLayout(1, 0));
+		controlPanel.add(this.buildFlipFlag1Button());
+		controlPanel.add(this.buildClearModelButton());
+		controlPanel.add(this.buildRestoreModelButton());
+		controlPanel.add(this.buildPrintModelButton());
+		return controlPanel;
+	}
+
+	private JButton buildFlipFlag1Button() {
+		return new JButton(this.buildFlipFlag1Action());
+	}
+
+	private Action buildFlipFlag1Action() {
+		Action action = new AbstractAction("flip flag 1") {
+			public void actionPerformed(ActionEvent event) {
+				CheckBoxModelAdapterUITest.this.flipFlag1();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void flipFlag1() {
+		this.testModel.setFlag1( ! this.testModel.isFlag1());
+	}
+
+	private JButton buildClearModelButton() {
+		return new JButton(this.buildClearModelAction());
+	}
+
+	private Action buildClearModelAction() {
+		Action action = new AbstractAction("clear model") {
+			public void actionPerformed(ActionEvent event) {
+				CheckBoxModelAdapterUITest.this.clearModel();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void clearModel() {
+		this.testModelHolder.setValue(null);
+	}
+
+	private JButton buildRestoreModelButton() {
+		return new JButton(this.buildRestoreModelAction());
+	}
+
+	private Action buildRestoreModelAction() {
+		Action action = new AbstractAction("restore model") {
+			public void actionPerformed(ActionEvent event) {
+				CheckBoxModelAdapterUITest.this.restoreModel();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void restoreModel() {
+		this.testModelHolder.setValue(this.testModel);
+	}
+
+	private JButton buildPrintModelButton() {
+		return new JButton(this.buildPrintModelAction());
+	}
+
+	private Action buildPrintModelAction() {
+		Action action = new AbstractAction("print model") {
+			public void actionPerformed(ActionEvent event) {
+				CheckBoxModelAdapterUITest.this.printModel();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void printModel() {
+		System.out.println("flag 1: " + this.testModel.isFlag1());
+		System.out.println("flag 2: " + this.testModel.isFlag2());
+		System.out.println("not flag 2: " + this.testModel.isNotFlag2());
+		System.out.println("***");
+	}
+
+
+	private class TestModel extends AbstractModel {
+		private boolean flag1;
+			public static final String FLAG1_PROPERTY = "flag1";
+		private boolean flag2;
+			public static final String FLAG2_PROPERTY = "flag2";
+		private boolean notFlag2;
+			public static final String NOT_FLAG2_PROPERTY = "notFlag2";
+	
+		public TestModel(boolean flag1, boolean flag2) {
+			this.flag1 = flag1;
+			this.flag2 = flag2;
+			this.notFlag2 = ! flag2;
+		}
+		public boolean isFlag1() {
+			return this.flag1;
+		}
+		public void setFlag1(boolean flag1) {
+			boolean old = this.flag1;
+			this.flag1 = flag1;
+			this.firePropertyChanged(FLAG1_PROPERTY, old, flag1);
+		}
+		public boolean isFlag2() {
+			return this.flag2;
+		}
+		public void setFlag2(boolean flag2) {
+			boolean old = this.flag2;
+			this.flag2 = flag2;
+			this.firePropertyChanged(FLAG2_PROPERTY, old, flag2);
+	
+			old = this.notFlag2;
+			this.notFlag2 = ! flag2;
+			this.firePropertyChanged(NOT_FLAG2_PROPERTY, old, this.notFlag2);
+		}
+		public boolean isNotFlag2() {
+			return this.notFlag2;
+		}
+		public void setNotFlag2(boolean notFlag2) {
+			this.setFlag2( ! notFlag2);
+		}
+		@Override
+		public String toString() {
+			return "TestModel(" + this.isFlag1() + " - " + this.isFlag2() + ")";
+		}
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ComboBoxModelAdapterTests.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ComboBoxModelAdapterTests.java
new file mode 100644
index 0000000..d12a5cb
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ComboBoxModelAdapterTests.java
@@ -0,0 +1,113 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.ListIterator;
+
+import javax.swing.ComboBoxModel;
+import javax.swing.ListModel;
+
+import org.eclipse.jpt.utility.internal.ClassTools;
+import org.eclipse.jpt.utility.internal.model.event.PropertyChangeEvent;
+import org.eclipse.jpt.utility.internal.model.listener.PropertyChangeListener;
+import org.eclipse.jpt.utility.internal.model.value.SimpleListValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimplePropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+import org.eclipse.jpt.utility.internal.model.value.swing.ComboBoxModelAdapter;
+import org.eclipse.jpt.utility.internal.swing.Displayable;
+import org.eclipse.jpt.utility.internal.swing.SimpleDisplayable;
+import org.eclipse.jpt.utility.tests.internal.model.value.SynchronizedList;
+
+import junit.framework.TestCase;
+
+public class ComboBoxModelAdapterTests extends TestCase {
+
+	public ComboBoxModelAdapterTests(String name) {
+		super(name);
+	}
+
+	@Override
+	protected void setUp() throws Exception {
+		super.setUp();
+		// nothing yet...
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		// nothing yet...
+		super.tearDown();
+	}
+
+	public void testHasListeners() throws Exception {
+		SimpleListValueModel listHolder = this.buildListHolder();
+		assertFalse(listHolder.hasAnyListChangeListeners(ValueModel.VALUE));
+		SimplePropertyValueModel selectionHolder = new SimplePropertyValueModel(((ListIterator) listHolder.getValue()).next());
+		assertFalse(selectionHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+
+		ComboBoxModel comboBoxModel = new ComboBoxModelAdapter(listHolder, selectionHolder);
+		assertFalse(listHolder.hasAnyListChangeListeners(ValueModel.VALUE));
+		assertFalse(selectionHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasNoListeners(comboBoxModel);
+
+		SynchronizedList synchList = new SynchronizedList(comboBoxModel);
+		PropertyChangeListener selectionListener = this.buildSelectionListener();
+		selectionHolder.addPropertyChangeListener(ValueModel.VALUE, selectionListener);
+		assertTrue(listHolder.hasAnyListChangeListeners(ValueModel.VALUE));
+		assertTrue(selectionHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasListeners(comboBoxModel);
+
+		comboBoxModel.removeListDataListener(synchList);
+		selectionHolder.removePropertyChangeListener(ValueModel.VALUE, selectionListener);
+		assertFalse(listHolder.hasAnyListChangeListeners(ValueModel.VALUE));
+		assertFalse(selectionHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasNoListeners(comboBoxModel);
+	}
+
+	private PropertyChangeListener buildSelectionListener() {
+		return new PropertyChangeListener() {
+			public void propertyChanged(PropertyChangeEvent evt) {
+				// do nothing...
+			}
+		};
+	}
+
+	private void verifyHasNoListeners(ListModel listModel) throws Exception {
+		boolean hasNoListeners = ((Boolean) ClassTools.executeMethod(listModel, "hasNoListDataListeners")).booleanValue();
+		assertTrue(hasNoListeners);
+	}
+
+	private void verifyHasListeners(ListModel listModel) throws Exception {
+		boolean hasListeners = ((Boolean) ClassTools.executeMethod(listModel, "hasListDataListeners")).booleanValue();
+		assertTrue(hasListeners);
+	}
+
+	private SimpleListValueModel buildListHolder() {
+		return new SimpleListValueModel(this.buildList());
+	}
+
+	private List<Displayable> buildList() {
+		List<Displayable> list = new ArrayList<Displayable>();
+		this.populateCollection(list);
+		return list;
+	}
+
+	private void populateCollection(Collection<Displayable> c) {
+		c.add(new SimpleDisplayable("foo"));
+		c.add(new SimpleDisplayable("bar"));
+		c.add(new SimpleDisplayable("baz"));
+		c.add(new SimpleDisplayable("joo"));
+		c.add(new SimpleDisplayable("jar"));
+		c.add(new SimpleDisplayable("jaz"));
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ComboBoxModelAdapterUITest.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ComboBoxModelAdapterUITest.java
new file mode 100644
index 0000000..8dec2ae
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ComboBoxModelAdapterUITest.java
@@ -0,0 +1,389 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.ComboBoxModel;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.ListCellRenderer;
+import javax.swing.UIManager;
+import javax.swing.WindowConstants;
+
+import org.eclipse.jpt.utility.internal.ClassTools;
+import org.eclipse.jpt.utility.internal.CollectionTools;
+import org.eclipse.jpt.utility.internal.model.AbstractModel;
+import org.eclipse.jpt.utility.internal.model.value.ListValueModel;
+import org.eclipse.jpt.utility.internal.model.value.PropertyAspectAdapter;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimpleListValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimplePropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+import org.eclipse.jpt.utility.internal.model.value.swing.ComboBoxModelAdapter;
+import org.eclipse.jpt.utility.internal.swing.FilteringListBrowser;
+import org.eclipse.jpt.utility.internal.swing.ListChooser;
+import org.eclipse.jpt.utility.internal.swing.SimpleListCellRenderer;
+
+
+/**
+ * Play around with a set of combo-boxes.
+ * 
+ * DefaultLongListBrowserDialogUITest subclasses this class; so be
+ * careful when making changes.
+ */
+public class ComboBoxModelAdapterUITest {
+
+	protected JFrame window;
+	private TestModel testModel;
+	private PropertyValueModel testModelHolder;
+	private PropertyValueModel colorHolder;
+	private ListValueModel colorListHolder;
+	protected ComboBoxModel colorComboBoxModel;
+	private int nextColorNumber = 0;
+
+	public static void main(String[] args) throws Exception {
+		new ComboBoxModelAdapterUITest().exec(args);
+	}
+
+	protected ComboBoxModelAdapterUITest() {
+		super();
+	}
+
+	protected void exec(String[] args) throws Exception {
+		UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+//		UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());	// Metal LAF
+//		UIManager.setLookAndFeel(com.sun.java.swing.plaf.windows.WindowsLookAndFeel.class.getName());
+//		UIManager.setLookAndFeel(com.sun.java.swing.plaf.motif.MotifLookAndFeel.class.getName());
+//		UIManager.setLookAndFeel(oracle.bali.ewt.olaf.OracleLookAndFeel.class.getName());
+		this.testModel = this.buildTestModel();
+		this.testModelHolder = new SimplePropertyValueModel(this.testModel);
+		this.colorHolder = this.buildColorHolder(this.testModelHolder);
+		this.colorListHolder = this.buildColorListHolder();
+		this.colorComboBoxModel = this.buildComboBoxModelAdapter(this.colorListHolder, this.colorHolder);
+		this.openWindow();
+	}
+
+	private PropertyValueModel buildColorHolder(ValueModel vm) {
+		return new PropertyAspectAdapter(vm, TestModel.COLOR_PROPERTY) {
+			@Override
+			protected Object getValueFromSubject() {
+				return ((TestModel) this.subject).getColor();
+			}
+			@Override
+			protected void setValueOnSubject(Object value) {
+				((TestModel) this.subject).setColor((String) value);
+			}
+		};
+	}
+
+	protected TestModel buildTestModel() {
+		return new TestModel();
+	}
+
+	protected ListValueModel buildColorListHolder() {
+		return new SimpleListValueModel(TestModel.validColors());
+//		return new AbstractReadOnlyListValueModel() {
+//			public Object getValue() {
+//				return new ArrayListIterator(TestModel.VALID_COLORS);
+//			}
+//			public int size() {
+//				return TestModel.VALID_COLORS.length;
+//			}
+//		};
+	}
+
+	private ComboBoxModel buildComboBoxModelAdapter(ListValueModel listHolder, PropertyValueModel selectionHolder) {
+		return new ComboBoxModelAdapter(listHolder, selectionHolder);
+	}
+
+	private void openWindow() {
+		this.window = new JFrame(ClassTools.shortNameFor(this.getClass()));
+		this.window.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
+		this.window.addWindowListener(this.buildWindowListener());
+		this.window.getContentPane().add(this.buildMainPanel(), "Center");
+		this.window.setLocation(300, 300);
+		this.window.setSize(400, 150);
+		this.window.setVisible(true);
+	}
+
+	private WindowListener buildWindowListener() {
+		return new WindowAdapter() {
+			@Override
+			public void windowClosing(WindowEvent e) {
+				e.getWindow().setVisible(false);
+				System.exit(0);
+			}
+		};
+	}
+
+	private Component buildMainPanel() {
+		JPanel mainPanel = new JPanel(new BorderLayout());
+		mainPanel.add(this.buildComboBoxPanel(), BorderLayout.NORTH);
+		mainPanel.add(this.buildControlPanel(), BorderLayout.SOUTH);
+		return mainPanel;
+	}
+
+	protected JPanel buildComboBoxPanel() {
+		JPanel panel = new JPanel(new GridLayout(1, 0));
+		panel.add(this.buildComboBox());
+		panel.add(this.buildComboBox());
+		panel.add(this.buildListChooser1());
+		panel.add(this.buildListChooser2());
+		return panel;
+	}
+
+	private JComboBox buildComboBox() {
+		JComboBox comboBox = new JComboBox(this.colorComboBoxModel);
+		comboBox.setRenderer(this.buildComboBoxRenderer());
+		return comboBox;
+	}
+
+	protected ListCellRenderer buildComboBoxRenderer() {
+		return new SimpleListCellRenderer() {
+			@Override
+			protected String buildText(Object value) {
+				return super.buildText(value);
+			}
+		};
+	}
+
+	private ListChooser buildListChooser1() {
+		return new LocalListChooser1(this.colorComboBoxModel);
+	}
+
+	private ListChooser buildListChooser2() {
+		return new LocalListChooser2(this.colorComboBoxModel);
+	}
+
+	private Component buildControlPanel() {
+		JPanel controlPanel = new JPanel(new GridLayout(2, 0));
+		controlPanel.add(this.buildResetColorButton());
+		controlPanel.add(this.buildClearModelButton());
+		controlPanel.add(this.buildRestoreModelButton());
+		controlPanel.add(this.buildPrintModelButton());
+		controlPanel.add(this.buildAddTenButton());
+		controlPanel.add(this.buildRemoveTenButton());
+		return controlPanel;
+	}
+
+	// ********** reset color button **********
+	private JButton buildResetColorButton() {
+		return new JButton(this.buildResetColorAction());
+	}
+
+	private Action buildResetColorAction() {
+		Action action = new AbstractAction("reset color") {
+			public void actionPerformed(ActionEvent event) {
+				ComboBoxModelAdapterUITest.this.resetColor();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void resetColor() {
+		this.testModel.setColor(TestModel.DEFAULT_COLOR);
+	}
+
+	// ********** clear model button **********
+	private JButton buildClearModelButton() {
+		return new JButton(this.buildClearModelAction());
+	}
+
+	private Action buildClearModelAction() {
+		Action action = new AbstractAction("clear model") {
+			public void actionPerformed(ActionEvent event) {
+				ComboBoxModelAdapterUITest.this.clearModel();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void clearModel() {
+		this.testModelHolder.setValue(null);
+	}
+
+	// ********** restore model button **********
+	private JButton buildRestoreModelButton() {
+		return new JButton(this.buildRestoreModelAction());
+	}
+
+	private Action buildRestoreModelAction() {
+		Action action = new AbstractAction("restore model") {
+			public void actionPerformed(ActionEvent event) {
+				ComboBoxModelAdapterUITest.this.restoreModel();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void restoreModel() {
+		this.testModelHolder.setValue(this.testModel);
+	}
+
+	// ********** print model button **********
+	private JButton buildPrintModelButton() {
+		return new JButton(this.buildPrintModelAction());
+	}
+
+	private Action buildPrintModelAction() {
+		Action action = new AbstractAction("print model") {
+			public void actionPerformed(ActionEvent event) {
+				ComboBoxModelAdapterUITest.this.printModel();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void printModel() {
+		System.out.println(this.testModel);
+	}
+
+	// ********** add 20 button **********
+	private JButton buildAddTenButton() {
+		return new JButton(this.buildAddTenAction());
+	}
+
+	private Action buildAddTenAction() {
+		Action action = new AbstractAction("add 20") {
+			public void actionPerformed(ActionEvent event) {
+				ComboBoxModelAdapterUITest.this.addTen();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void addTen() {
+		for (int i = this.nextColorNumber; i < this.nextColorNumber + 20; i++) {
+			this.colorListHolder.addItem(this.colorListHolder.size(), "color" + i);
+		}
+		this.nextColorNumber += 20;
+	}
+
+	// ********** remove 20 button **********
+	private JButton buildRemoveTenButton() {
+		return new JButton(this.buildRemoveTenAction());
+	}
+
+	private Action buildRemoveTenAction() {
+		Action action = new AbstractAction("remove 20") {
+			public void actionPerformed(ActionEvent event) {
+				ComboBoxModelAdapterUITest.this.removeTen();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void removeTen() {
+		for (int i = 0; i < 20; i++) {
+			if (this.colorListHolder.size() > 0) {
+				this.colorListHolder.removeItem(this.colorListHolder.size() - 1);
+			}
+		}
+	}
+
+
+	protected static class TestModel extends AbstractModel {
+		private String color;
+			public static final String COLOR_PROPERTY = "color";
+			public static final String RED = "red";
+			public static final String ORANGE = "orange";
+			public static final String YELLOW = "yellow";
+			public static final String GREEN = "green";
+			public static final String BLUE = "blue";
+			public static final String INDIGO = "indigo";
+			public static final String VIOLET = "violet";
+			public static final String DEFAULT_COLOR = RED;
+			public static List<String> validColors;
+			public static final String[] DEFAULT_VALID_COLORS = {
+				RED,
+				ORANGE,
+				YELLOW,
+				GREEN,
+				BLUE,
+				INDIGO,
+				VIOLET
+			};
+	
+		public static List<String> validColors() {
+			if (validColors == null) {
+				validColors = buildDefaultValidColors();
+			}
+			return validColors;
+		}
+		public static List<String> buildDefaultValidColors() {
+			List<String> result = new ArrayList<String>();
+			CollectionTools.addAll(result, DEFAULT_VALID_COLORS);
+			return result;
+		}
+	
+		public TestModel() {
+			this(DEFAULT_COLOR);
+		}
+		public TestModel(String color) {
+			this.color = color;
+		}
+		public String getColor() {
+			return this.color;
+		}
+		public void setColor(String color) {
+			this.checkColor(color);
+			Object old = this.color;
+			this.color = color;
+			this.firePropertyChanged(COLOR_PROPERTY, old, color);
+		}
+		public void checkColor(String c) {
+			if ( ! validColors().contains(c)) {
+				throw new IllegalArgumentException(c);
+			}
+		}
+		@Override
+		public String toString() {
+			return "TestModel(" + this.color + ")";
+		}
+	}
+
+
+	private class LocalListChooser1 extends ListChooser {
+		public LocalListChooser1(ComboBoxModel model) {
+			super(model);
+		}
+	}
+
+
+	private class LocalListChooser2 extends ListChooser {
+		public LocalListChooser2(ComboBoxModel model) {
+			super(model);
+		}
+		@Override
+		protected ListBrowser buildBrowser() {
+			return new FilteringListBrowser();
+		}
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ComboBoxModelAdapterUITest2.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ComboBoxModelAdapterUITest2.java
new file mode 100644
index 0000000..db86b84
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ComboBoxModelAdapterUITest2.java
@@ -0,0 +1,75 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import javax.swing.ListCellRenderer;
+
+import org.eclipse.jpt.utility.internal.model.value.ExtendedListValueModelWrapper;
+import org.eclipse.jpt.utility.internal.model.value.ListValueModel;
+import org.eclipse.jpt.utility.internal.swing.SimpleListCellRenderer;
+
+/**
+ * 
+ */
+public class ComboBoxModelAdapterUITest2 extends ComboBoxModelAdapterUITest {
+
+	public static void main(String[] args) throws Exception {
+		new ComboBoxModelAdapterUITest2().exec(args);
+	}
+
+	public ComboBoxModelAdapterUITest2() {
+		super();
+	}
+
+	/**
+	 * add a null to the front of the list
+	 */
+	@Override
+	protected ListValueModel buildColorListHolder() {
+		// the default is to prepend the wrapped list with a null item
+		return new ExtendedListValueModelWrapper(super.buildColorListHolder());
+	}
+
+	/**
+	 * use a different model that allows the color to be set to null
+	 */
+	@Override
+	protected TestModel buildTestModel() {
+		return new TestModel2();
+	}
+
+	/**
+	 * convert null to some text
+	 */
+	@Override
+	protected ListCellRenderer buildComboBoxRenderer() {
+		return new SimpleListCellRenderer() {
+			@Override
+			protected String buildText(Object value) {
+				return (value == null) ? "<none selected>" : super.buildText(value);
+			}
+		};
+	}
+
+
+	protected static class TestModel2 extends TestModel {
+		/**
+		 * null is OK here
+		 */
+		@Override
+		public void checkColor(String color) {
+			if (color == null) {
+				return;
+			}
+			super.checkColor(color);
+		}
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/DateSpinnerModelAdapterTests.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/DateSpinnerModelAdapterTests.java
new file mode 100644
index 0000000..bdad5b7
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/DateSpinnerModelAdapterTests.java
@@ -0,0 +1,151 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import java.util.Date;
+
+import javax.swing.SpinnerModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimplePropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+import org.eclipse.jpt.utility.internal.model.value.swing.DateSpinnerModelAdapter;
+import org.eclipse.jpt.utility.tests.internal.TestTools;
+
+import junit.framework.TestCase;
+
+public class DateSpinnerModelAdapterTests extends TestCase {
+	private PropertyValueModel valueHolder;
+	private SpinnerModel spinnerModelAdapter;
+	boolean eventFired;
+
+	public DateSpinnerModelAdapterTests(String name) {
+		super(name);
+	}
+
+	@Override
+	protected void setUp() throws Exception {
+		super.setUp();
+		this.valueHolder = new SimplePropertyValueModel(new Date());
+		this.spinnerModelAdapter = new DateSpinnerModelAdapter(this.valueHolder);
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		TestTools.clear(this);
+		super.tearDown();
+	}
+
+	public void testSetValueSpinnerModel() throws Exception {
+		this.eventFired = false;
+		this.spinnerModelAdapter.addChangeListener(new TestChangeListener() {
+			@Override
+			public void stateChanged(ChangeEvent e) {
+				DateSpinnerModelAdapterTests.this.eventFired = true;
+			}
+		});
+		Date newDate = new Date();
+		newDate.setTime(777777);
+		this.spinnerModelAdapter.setValue(newDate);
+		assertTrue(this.eventFired);
+		assertEquals(777777, ((Date) this.valueHolder.getValue()).getTime());
+	}
+
+	public void testSetValueValueHolder() throws Exception {
+		this.eventFired = false;
+		this.spinnerModelAdapter.addChangeListener(new TestChangeListener() {
+			@Override
+			public void stateChanged(ChangeEvent e) {
+				DateSpinnerModelAdapterTests.this.eventFired = true;
+			}
+		});
+		Date newDate = new Date();
+		newDate.setTime(777777);
+		this.valueHolder.setValue(newDate);
+		assertTrue(this.eventFired);
+		assertEquals(777777, ((Date) this.spinnerModelAdapter.getValue()).getTime());
+	}
+
+	public void testDefaultValue() throws Exception {
+		Date newDate = new Date();
+		newDate.setTime(777777);
+		this.valueHolder.setValue(newDate);
+		this.eventFired = false;
+		this.spinnerModelAdapter.addChangeListener(new TestChangeListener() {
+			@Override
+			public void stateChanged(ChangeEvent e) {
+				DateSpinnerModelAdapterTests.this.eventFired = true;
+			}
+		});
+		assertEquals(777777, ((Date) this.spinnerModelAdapter.getValue()).getTime());
+		this.valueHolder.setValue(null);
+		assertTrue(this.eventFired);
+		assertFalse(((Date) this.spinnerModelAdapter.getValue()).getTime() == 777777);
+	}
+
+	public void testHasListeners() throws Exception {
+		SimplePropertyValueModel localValueHolder = (SimplePropertyValueModel) this.valueHolder;
+		assertFalse(localValueHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasNoListeners(this.spinnerModelAdapter);
+
+		ChangeListener listener = new TestChangeListener();
+		this.spinnerModelAdapter.addChangeListener(listener);
+		assertTrue(localValueHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasListeners(this.spinnerModelAdapter);
+
+		this.spinnerModelAdapter.removeChangeListener(listener);
+		assertFalse(localValueHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasNoListeners(this.spinnerModelAdapter);
+	}
+
+	private void verifyHasNoListeners(SpinnerModel adapter) throws Exception {
+		assertEquals(0, ((DateSpinnerModelAdapter) adapter).getChangeListeners().length);
+	}
+
+	private void verifyHasListeners(Object adapter) throws Exception {
+		assertFalse(((DateSpinnerModelAdapter) adapter).getChangeListeners().length == 0);
+	}
+
+	public void testNullInitialValue() {
+		Date today = new Date();
+		this.valueHolder = new SimplePropertyValueModel();
+		this.spinnerModelAdapter = new DateSpinnerModelAdapter(this.valueHolder, today);
+
+		this.eventFired = false;
+		this.spinnerModelAdapter.addChangeListener(new TestChangeListener() {
+			@Override
+			public void stateChanged(ChangeEvent e) {
+				DateSpinnerModelAdapterTests.this.eventFired = true;
+			}
+		});
+		assertEquals(today, this.spinnerModelAdapter.getValue());
+
+		Date newDate = new Date();
+		newDate.setTime(777777);
+		this.valueHolder.setValue(newDate);
+
+		assertTrue(this.eventFired);
+		assertEquals(777777, ((Date) this.spinnerModelAdapter.getValue()).getTime());
+	}
+
+
+	// ********** inner class **********
+	private class TestChangeListener implements ChangeListener {
+		TestChangeListener() {
+			super();
+		}
+		public void stateChanged(ChangeEvent e) {
+			fail("unexpected event");
+		}
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/DocumentAdapterTests.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/DocumentAdapterTests.java
new file mode 100644
index 0000000..da8d87a
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/DocumentAdapterTests.java
@@ -0,0 +1,154 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.event.DocumentEvent.EventType;
+import javax.swing.text.Document;
+
+import org.eclipse.jpt.utility.internal.ClassTools;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimplePropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+import org.eclipse.jpt.utility.internal.model.value.swing.DocumentAdapter;
+import org.eclipse.jpt.utility.tests.internal.TestTools;
+
+import junit.framework.TestCase;
+
+public class DocumentAdapterTests extends TestCase {
+	private PropertyValueModel stringHolder;
+	Document documentAdapter;
+	boolean eventFired;
+
+	public DocumentAdapterTests(String name) {
+		super(name);
+	}
+
+	@Override
+	protected void setUp() throws Exception {
+		super.setUp();
+		this.stringHolder = new SimplePropertyValueModel("0123456789");
+		this.documentAdapter = new DocumentAdapter(this.stringHolder);
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		TestTools.clear(this);
+		super.tearDown();
+	}
+
+	public void testRemove() throws Exception {
+		this.eventFired = false;
+		this.documentAdapter.addDocumentListener(new TestDocumentListener() {
+			@Override
+			public void removeUpdate(DocumentEvent e) {
+				DocumentAdapterTests.this.eventFired = true;
+				assertEquals(EventType.REMOVE, e.getType());
+				assertEquals(DocumentAdapterTests.this.documentAdapter, e.getDocument());
+				// this will be the removal of "23456"
+				assertEquals(2, e.getOffset());
+				assertEquals(5, e.getLength());
+			}
+		});
+		this.documentAdapter.remove(2, 5);
+		assertTrue(this.eventFired);
+		assertEquals("01789", this.stringHolder.getValue());
+	}
+
+	public void testInsert() throws Exception {
+		this.eventFired = false;
+		this.documentAdapter.addDocumentListener(new TestDocumentListener() {
+			@Override
+			public void insertUpdate(DocumentEvent e) {
+				DocumentAdapterTests.this.eventFired = true;
+				assertEquals(EventType.INSERT, e.getType());
+				assertEquals(DocumentAdapterTests.this.documentAdapter, e.getDocument());
+				// this will be the insert of "xxxxxx"
+				assertEquals(2, e.getOffset());
+				assertEquals(5, e.getLength());
+			}
+		});
+		this.documentAdapter.insertString(2, "xxxxx", null);
+		assertTrue(this.eventFired);
+		assertEquals("01xxxxx23456789", this.stringHolder.getValue());
+	}
+
+	public void testSetValue() throws Exception {
+		this.eventFired = false;
+		this.documentAdapter.addDocumentListener(new TestDocumentListener() {
+			@Override
+			public void insertUpdate(DocumentEvent e) {
+				DocumentAdapterTests.this.eventFired = true;
+				assertEquals(EventType.INSERT, e.getType());
+				assertEquals(DocumentAdapterTests.this.documentAdapter, e.getDocument());
+				// this will be the insert of "foo"
+				assertEquals(0, e.getOffset());
+				assertEquals(3, e.getLength());
+			}
+			@Override
+			public void removeUpdate(DocumentEvent e) {
+				assertEquals(EventType.REMOVE, e.getType());
+				assertEquals(DocumentAdapterTests.this.documentAdapter, e.getDocument());
+				// this will be the removal of "0123456789"
+				assertEquals(0, e.getOffset());
+				assertEquals(10, e.getLength());
+			}
+		});
+		assertEquals("0123456789", this.documentAdapter.getText(0, this.documentAdapter.getLength()));
+		this.stringHolder.setValue("foo");
+		assertTrue(this.eventFired);
+		assertEquals("foo", this.documentAdapter.getText(0, this.documentAdapter.getLength()));
+	}
+
+	public void testHasListeners() throws Exception {
+		SimplePropertyValueModel localStringHolder = (SimplePropertyValueModel) this.stringHolder;
+		assertFalse(localStringHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasNoListeners(this.documentAdapter);
+
+		DocumentListener listener = new TestDocumentListener();
+		this.documentAdapter.addDocumentListener(listener);
+		assertTrue(localStringHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasListeners(this.documentAdapter);
+
+		this.documentAdapter.removeDocumentListener(listener);
+		assertFalse(localStringHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasNoListeners(this.documentAdapter);
+	}
+
+	private void verifyHasNoListeners(Object document) throws Exception {
+		Object delegate = ClassTools.getFieldValue(document, "delegate");
+		Object[] listeners = (Object[]) ClassTools.executeMethod(delegate, "getDocumentListeners");
+		assertEquals(0, listeners.length);
+	}
+
+	private void verifyHasListeners(Object document) throws Exception {
+		Object delegate = ClassTools.getFieldValue(document, "delegate");
+		Object[] listeners = (Object[]) ClassTools.executeMethod(delegate, "getDocumentListeners");
+		assertFalse(listeners.length == 0);
+	}
+
+
+private class TestDocumentListener implements DocumentListener {
+	TestDocumentListener() {
+		super();
+	}
+	public void changedUpdate(DocumentEvent e) {
+		fail("unexpected event");
+	}
+	public void insertUpdate(DocumentEvent e) {
+		fail("unexpected event");
+	}
+	public void removeUpdate(DocumentEvent e) {
+		fail("unexpected event");
+	}
+}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/DocumentAdapterUITest.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/DocumentAdapterUITest.java
new file mode 100644
index 0000000..d6343bd
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/DocumentAdapterUITest.java
@@ -0,0 +1,257 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.JTextField;
+import javax.swing.WindowConstants;
+import javax.swing.text.AbstractDocument;
+import javax.swing.text.AttributeSet;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Document;
+import javax.swing.text.PlainDocument;
+
+import org.eclipse.jpt.utility.internal.model.AbstractModel;
+import org.eclipse.jpt.utility.internal.model.value.PropertyAspectAdapter;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimplePropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+import org.eclipse.jpt.utility.internal.model.value.swing.DocumentAdapter;
+
+/**
+ * Play around with a set of entry fields.
+ */
+public class DocumentAdapterUITest {
+
+	private TestModel testModel;
+		private static final String DEFAULT_NAME = "Scooby Doo";
+	private PropertyValueModel testModelHolder;
+	private PropertyValueModel nameHolder;
+	private Document nameDocument;
+	private Document upperCaseNameDocument;
+
+	public static void main(String[] args) throws Exception {
+		new DocumentAdapterUITest().exec(args);
+	}
+
+	private DocumentAdapterUITest() {
+		super();
+	}
+
+	private void exec(String[] args) throws Exception {
+		this.testModel = new TestModel(DEFAULT_NAME);
+		this.testModelHolder = new SimplePropertyValueModel(this.testModel);
+		this.nameHolder = this.buildNameHolder(this.testModelHolder);
+		this.nameDocument = this.buildNameDocument(this.nameHolder);
+		this.upperCaseNameDocument = this.buildUpperCaseNameDocument(this.nameHolder);
+		this.openWindow();
+	}
+
+	private PropertyValueModel buildNameHolder(ValueModel vm) {
+		return new PropertyAspectAdapter(vm, TestModel.NAME_PROPERTY) {
+			@Override
+			protected Object getValueFromSubject() {
+				return ((TestModel) this.subject).getName();
+			}
+			@Override
+			protected void setValueOnSubject(Object value) {
+				((TestModel) this.subject).setName((String) value);
+			}
+		};
+	}
+
+	private Document buildNameDocument(PropertyValueModel stringHolder) {
+		return new DocumentAdapter(stringHolder);
+	}
+
+	private Document buildUpperCaseNameDocument(PropertyValueModel stringHolder) {
+		return new DocumentAdapter(stringHolder, this.buildUpperCaseNameDocumentDelegate());
+	}
+
+	private AbstractDocument buildUpperCaseNameDocumentDelegate() {
+		return new PlainDocument() {
+			@Override
+			public void insertString(int offset, String string, AttributeSet a) throws BadLocationException {
+				if (string == null) {
+					return;
+				}
+				char[] upper = string.toCharArray();
+				for (int i = 0; i < upper.length; i++) {
+					upper[i] = Character.toUpperCase(upper[i]);
+				}
+				super.insertString(offset, new String(upper), a);
+			}
+		};
+	}
+
+	private void openWindow() {
+		JFrame window = new JFrame(this.getClass().getName());
+		window.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
+		window.addWindowListener(this.buildWindowListener());
+		window.getContentPane().add(this.buildMainPanel(), "Center");
+		window.setSize(400, 100);
+		window.setVisible(true);
+	}
+
+	private WindowListener buildWindowListener() {
+		return new WindowAdapter() {
+			@Override
+			public void windowClosing(WindowEvent e) {
+				e.getWindow().setVisible(false);
+				System.exit(0);
+			}
+		};
+	}
+
+	private Component buildMainPanel() {
+		JPanel mainPanel = new JPanel(new BorderLayout());
+		mainPanel.add(this.buildTextFieldPanel(), BorderLayout.NORTH);
+		mainPanel.add(this.buildControlPanel(), BorderLayout.SOUTH);
+		return mainPanel;
+	}
+
+	private Component buildTextFieldPanel() {
+		JPanel taskListPanel = new JPanel(new GridLayout(1, 0));
+		taskListPanel.add(this.buildNameTextField());
+		taskListPanel.add(this.buildReadOnlyNameTextField());
+		taskListPanel.add(this.buildUpperCaseNameTextField());
+		return taskListPanel;
+	}
+
+	private JTextField buildNameTextField() {
+		return new JTextField(this.nameDocument, null, 0);
+	}
+
+	private JTextField buildReadOnlyNameTextField() {
+		JTextField nameTextField = this.buildNameTextField();
+		nameTextField.setEditable(false);
+		return nameTextField;
+	}
+
+	private JTextField buildUpperCaseNameTextField() {
+		return new JTextField(this.upperCaseNameDocument, null, 0);
+	}
+
+	private Component buildControlPanel() {
+		JPanel controlPanel = new JPanel(new GridLayout(1, 0));
+		controlPanel.add(this.buildResetNameButton());
+		controlPanel.add(this.buildClearModelButton());
+		controlPanel.add(this.buildRestoreModelButton());
+		controlPanel.add(this.buildPrintModelButton());
+		return controlPanel;
+	}
+
+	private JButton buildResetNameButton() {
+		return new JButton(this.buildResetNameAction());
+	}
+
+	private Action buildResetNameAction() {
+		Action action = new AbstractAction("reset name") {
+			public void actionPerformed(ActionEvent event) {
+				DocumentAdapterUITest.this.resetName();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void resetName() {
+		this.testModel.setName(DEFAULT_NAME);
+	}
+
+	private JButton buildClearModelButton() {
+		return new JButton(this.buildClearModelAction());
+	}
+
+	private Action buildClearModelAction() {
+		Action action = new AbstractAction("clear model") {
+			public void actionPerformed(ActionEvent event) {
+				DocumentAdapterUITest.this.clearModel();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void clearModel() {
+		this.testModelHolder.setValue(null);
+	}
+
+	private JButton buildRestoreModelButton() {
+		return new JButton(this.buildRestoreModelAction());
+	}
+
+	private Action buildRestoreModelAction() {
+		Action action = new AbstractAction("restore model") {
+			public void actionPerformed(ActionEvent event) {
+				DocumentAdapterUITest.this.restoreModel();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void restoreModel() {
+		this.testModelHolder.setValue(this.testModel);
+	}
+
+	private JButton buildPrintModelButton() {
+		return new JButton(this.buildPrintModelAction());
+	}
+
+	private Action buildPrintModelAction() {
+		Action action = new AbstractAction("print model") {
+			public void actionPerformed(ActionEvent event) {
+				DocumentAdapterUITest.this.printModel();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void printModel() {
+		System.out.println("name: " + this.testModel.getName());
+	}
+
+
+	private class TestModel extends AbstractModel {
+		private String name;
+			public static final String NAME_PROPERTY = "name";
+	
+		public TestModel(String name) {
+			this.name = name;
+		}
+		public String getName() {
+			return this.name;
+		}
+		public void setName(String name) {
+			Object old = this.name;
+			this.name = name;
+			this.firePropertyChanged(NAME_PROPERTY, old, name);
+		}
+		@Override
+		public String toString() {
+			return "TestModel(" + this.getName() + ")";
+		}
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/JptUtilityModelValueSwingTests.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/JptUtilityModelValueSwingTests.java
new file mode 100644
index 0000000..b531bd3
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/JptUtilityModelValueSwingTests.java
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import junit.framework.Test;
+import junit.framework.TestSuite;
+
+public class JptUtilityModelValueSwingTests {
+	
+	public static Test suite() {
+		TestSuite suite = new TestSuite(JptUtilityModelValueSwingTests.class.getPackage().getName());
+
+		suite.addTestSuite(CheckBoxModelAdapterTests.class);
+		suite.addTestSuite(ComboBoxModelAdapterTests.class);
+		suite.addTestSuite(DateSpinnerModelAdapterTests.class);
+		suite.addTestSuite(DocumentAdapterTests.class);
+		suite.addTestSuite(ListModelAdapterTests.class);
+		suite.addTestSuite(ListSpinnerModelAdapterTests.class);
+		suite.addTestSuite(NumberSpinnerModelAdapterTests.class);
+		suite.addTestSuite(ObjectListSelectionModelTests.class);
+		suite.addTestSuite(PrimitiveListTreeModelTests.class);
+		suite.addTestSuite(RadioButtonModelAdapterTests.class);
+		suite.addTestSuite(SpinnerModelAdapterTests.class);
+		suite.addTestSuite(TableModelAdapterTests.class);
+		suite.addTestSuite(TreeModelAdapterTests.class);
+	
+		return suite;
+	}
+	
+	private JptUtilityModelValueSwingTests() {
+		super();
+		throw new UnsupportedOperationException();
+	}
+	
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ListModelAdapterTests.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ListModelAdapterTests.java
new file mode 100644
index 0000000..a66c76c
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ListModelAdapterTests.java
@@ -0,0 +1,300 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import javax.swing.ListModel;
+
+import org.eclipse.jpt.utility.internal.Bag;
+import org.eclipse.jpt.utility.internal.ClassTools;
+import org.eclipse.jpt.utility.internal.HashBag;
+import org.eclipse.jpt.utility.internal.model.value.CollectionValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ListValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimpleCollectionValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimpleListValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SortedListValueModelAdapter;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+import org.eclipse.jpt.utility.internal.model.value.swing.ListModelAdapter;
+import org.eclipse.jpt.utility.tests.internal.model.value.SynchronizedList;
+
+import junit.framework.TestCase;
+
+public class ListModelAdapterTests extends TestCase {
+
+	public ListModelAdapterTests(String name) {
+		super(name);
+	}
+
+	@Override
+	protected void setUp() throws Exception {
+		super.setUp();
+		// nothing yet...
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		// nothing yet...
+		super.tearDown();
+	}
+
+	public void testCollectionSynchronization() {
+		CollectionValueModel collectionHolder = this.buildCollectionHolder();
+		ListModel listModel = new ListModelAdapter(collectionHolder);
+		SynchronizedList synchList = new SynchronizedList(listModel);
+		assertEquals(6, synchList.size());
+		this.compare(listModel, synchList);
+
+		collectionHolder.addItem("tom");
+		collectionHolder.addItem("dick");
+		collectionHolder.addItem("harry");
+		collectionHolder.addItem(null);
+		assertEquals(10, synchList.size());
+		this.compare(listModel, synchList);
+
+		collectionHolder.removeItem("foo");
+		collectionHolder.removeItem("jar");
+		collectionHolder.removeItem("harry");
+		collectionHolder.removeItem(null);
+		assertEquals(6, synchList.size());
+		this.compare(listModel, synchList);
+	}
+
+	public void testListSynchronization() {
+		ListValueModel listHolder = this.buildListHolder();
+		ListModel listModel = new ListModelAdapter(listHolder);
+		SynchronizedList synchList = new SynchronizedList(listModel);
+		assertEquals(6, synchList.size());
+		this.compare(listModel, synchList);
+
+		listHolder.addItem(6, "tom");
+		listHolder.addItem(7, "dick");
+		listHolder.addItem(8, "harry");
+		listHolder.addItem(9, null);
+		assertEquals(10, synchList.size());
+		this.compare(listModel, synchList);
+
+		listHolder.removeItem(9);
+		listHolder.removeItem(8);
+		listHolder.removeItem(4);
+		listHolder.removeItem(0);
+		assertEquals(6, synchList.size());
+		this.compare(listModel, synchList);
+	}
+
+	public void testSetModel() {
+		SimpleListValueModel listHolder1 = this.buildListHolder();
+		ListModelAdapter listModel = new ListModelAdapter(listHolder1);
+		SynchronizedList synchList = new SynchronizedList(listModel);
+		assertTrue(listHolder1.hasAnyListChangeListeners(ValueModel.VALUE));
+		assertEquals(6, synchList.size());
+		this.compare(listModel, synchList);
+
+		SimpleListValueModel listHolder2 = this.buildListHolder2();
+		listModel.setModel(listHolder2);
+		assertEquals(3, synchList.size());
+		this.compare(listModel, synchList);
+		assertTrue(listHolder1.hasNoListChangeListeners(ValueModel.VALUE));
+		assertTrue(listHolder2.hasAnyListChangeListeners(ValueModel.VALUE));
+
+		listModel.setModel(new SimpleListValueModel());
+		assertEquals(0, synchList.size());
+		this.compare(listModel, synchList);
+		assertTrue(listHolder1.hasNoListChangeListeners(ValueModel.VALUE));
+		assertTrue(listHolder2.hasNoListChangeListeners(ValueModel.VALUE));
+	}
+
+	private void compare(ListModel listModel, List list) {
+		assertEquals(listModel.getSize(), list.size());
+		for (int i = 0; i < listModel.getSize(); i++) {
+			assertEquals(listModel.getElementAt(i), list.get(i));
+		}
+	}
+
+	public void testCollectionSort() {
+		this.verifyCollectionSort(null);
+	}
+
+	public void testListSort() {
+		this.verifyListSort(null);
+	}
+
+	public void testCustomCollectionSort() {
+		this.verifyCollectionSort(this.buildCustomComparator());
+	}
+
+	public void testCustomListSort() {
+		this.verifyListSort(this.buildCustomComparator());
+	}
+
+	private Comparator buildCustomComparator() {
+		// sort with reverse order
+		return new Comparator() {
+			public int compare(Object o1, Object o2) {
+				return ((Comparable) o2).compareTo(o1);
+			}
+		};
+	}
+
+	private void verifyCollectionSort(Comparator comparator) {
+		CollectionValueModel collectionHolder = this.buildCollectionHolder();
+		ListModel listModel = new ListModelAdapter(new SortedListValueModelAdapter(collectionHolder, comparator));
+		SynchronizedList synchList = new SynchronizedList(listModel);
+		assertEquals(6, synchList.size());
+		this.compareSort(listModel, synchList, comparator);
+
+		collectionHolder.addItem("tom");
+		collectionHolder.addItem("dick");
+		collectionHolder.addItem("harry");
+		assertEquals(9, synchList.size());
+		this.compareSort(listModel, synchList, comparator);
+
+		collectionHolder.removeItem("foo");
+		collectionHolder.removeItem("jar");
+		collectionHolder.removeItem("harry");
+		assertEquals(6, synchList.size());
+		this.compareSort(listModel, synchList, comparator);
+	}
+
+	private void verifyListSort(Comparator comparator) {
+		ListValueModel listHolder = this.buildListHolder();
+		ListModel listModel = new ListModelAdapter(new SortedListValueModelAdapter(listHolder, comparator));
+		SynchronizedList synchList = new SynchronizedList(listModel);
+		assertEquals(6, synchList.size());
+		this.compareSort(listModel, synchList, comparator);
+
+		listHolder.addItem(0, "tom");
+		listHolder.addItem(0, "dick");
+		listHolder.addItem(0, "harry");
+		assertEquals(9, synchList.size());
+		this.compareSort(listModel, synchList, comparator);
+
+		listHolder.removeItem(8);
+		listHolder.removeItem(4);
+		listHolder.removeItem(0);
+		listHolder.removeItem(5);
+		assertEquals(5, synchList.size());
+		this.compareSort(listModel, synchList, comparator);
+	}
+
+	private void compareSort(ListModel listModel, List list, Comparator comparator) {
+		SortedSet ss = new TreeSet(comparator);
+		for (int i = 0; i < listModel.getSize(); i++) {
+			ss.add(listModel.getElementAt(i));
+		}
+		assertEquals(ss.size(), list.size());
+		for (Iterator stream1 = ss.iterator(), stream2 = list.iterator(); stream1.hasNext(); ) {
+			assertEquals(stream1.next(), stream2.next());
+		}
+	}
+
+	public void testHasListeners() throws Exception {
+		SimpleListValueModel listHolder = this.buildListHolder();
+		assertFalse(listHolder.hasAnyListChangeListeners(ValueModel.VALUE));
+
+		ListModel listModel = new ListModelAdapter(listHolder);
+		assertFalse(listHolder.hasAnyListChangeListeners(ValueModel.VALUE));
+		this.verifyHasNoListeners(listModel);
+
+		SynchronizedList synchList = new SynchronizedList(listModel);
+		assertTrue(listHolder.hasAnyListChangeListeners(ValueModel.VALUE));
+		this.verifyHasListeners(listModel);
+
+		listModel.removeListDataListener(synchList);
+		assertFalse(listHolder.hasAnyListChangeListeners(ValueModel.VALUE));
+		this.verifyHasNoListeners(listModel);
+	}
+
+	public void testGetSize() throws Exception {
+		SimpleListValueModel listHolder = this.buildListHolder();
+		ListModel listModel = new ListModelAdapter(listHolder);
+		this.verifyHasNoListeners(listModel);
+		assertEquals(6, listModel.getSize());
+
+		SynchronizedList synchList = new SynchronizedList(listModel);
+		this.verifyHasListeners(listModel);
+		assertEquals(6, listModel.getSize());
+
+		listModel.removeListDataListener(synchList);
+		this.verifyHasNoListeners(listModel);
+		assertEquals(6, listModel.getSize());
+	}
+
+	public void testGetElementAt() throws Exception {
+		SimpleListValueModel listHolder = this.buildListHolder();
+		ListModel listModel = new ListModelAdapter(new SortedListValueModelAdapter(listHolder));
+		SynchronizedList synchList = new SynchronizedList(listModel);
+		this.verifyHasListeners(listModel);
+		assertEquals("bar", listModel.getElementAt(0));
+		assertEquals("bar", synchList.get(0));
+	}
+
+	private void verifyHasNoListeners(ListModel listModel) throws Exception {
+		boolean hasNoListeners = ((Boolean) ClassTools.executeMethod(listModel, "hasNoListDataListeners")).booleanValue();
+		assertTrue(hasNoListeners);
+	}
+
+	private void verifyHasListeners(ListModel listModel) throws Exception {
+		boolean hasListeners = ((Boolean) ClassTools.executeMethod(listModel, "hasListDataListeners")).booleanValue();
+		assertTrue(hasListeners);
+	}
+
+	private CollectionValueModel buildCollectionHolder() {
+		return new SimpleCollectionValueModel(this.buildCollection());
+	}
+
+	private Collection<String> buildCollection() {
+		Bag<String> bag = new HashBag<String>();
+		this.populateCollection(bag);
+		return bag;
+	}
+
+	private SimpleListValueModel buildListHolder() {
+		return new SimpleListValueModel(this.buildList());
+	}
+
+	private List<String> buildList() {
+		List<String> list = new ArrayList<String>();
+		this.populateCollection(list);
+		return list;
+	}
+
+	private void populateCollection(Collection<String> c) {
+		c.add("foo");
+		c.add("bar");
+		c.add("baz");
+		c.add("joo");
+		c.add("jar");
+		c.add("jaz");
+	}
+
+	private SimpleListValueModel buildListHolder2() {
+		return new SimpleListValueModel(this.buildList2());
+	}
+
+	private List<String> buildList2() {
+		List<String> list = new ArrayList<String>();
+		this.populateCollection2(list);
+		return list;
+	}
+
+	private void populateCollection2(Collection<String> c) {
+		c.add("tom");
+		c.add("dick");
+		c.add("harry");
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ListModelAdapterUITest.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ListModelAdapterUITest.java
new file mode 100644
index 0000000..2c45a4f
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ListModelAdapterUITest.java
@@ -0,0 +1,363 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.GridLayout;
+import java.awt.TextField;
+import java.awt.event.ActionEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.ListIterator;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.Icon;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.ListModel;
+import javax.swing.WindowConstants;
+
+import org.eclipse.jpt.utility.internal.model.AbstractModel;
+import org.eclipse.jpt.utility.internal.model.value.ListAspectAdapter;
+import org.eclipse.jpt.utility.internal.model.value.ListValueModel;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimplePropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SortedListValueModelAdapter;
+import org.eclipse.jpt.utility.internal.model.value.swing.ListModelAdapter;
+import org.eclipse.jpt.utility.internal.swing.Displayable;
+
+/**
+ * an example UI for testing various permutations of the ListModelAdapter
+ */
+public class ListModelAdapterUITest {
+
+	private PropertyValueModel taskListHolder;
+	private TextField taskTextField;
+
+	public static void main(String[] args) throws Exception {
+		new ListModelAdapterUITest().exec(args);
+	}
+
+	private ListModelAdapterUITest() {
+		super();
+	}
+
+	private void exec(String[] args) throws Exception {
+		this.taskListHolder = new SimplePropertyValueModel(new TaskList());
+		this.openWindow();
+	}
+
+	private void openWindow() {
+		JFrame window = new JFrame(this.getClass().getName());
+		window.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
+		window.addWindowListener(this.buildWindowListener());
+		window.getContentPane().add(this.buildMainPanel(), "Center");
+		window.setSize(800, 400);
+		window.setVisible(true);
+	}
+
+	private WindowListener buildWindowListener() {
+		return new WindowAdapter() {
+			@Override
+			public void windowClosing(WindowEvent e) {
+				e.getWindow().setVisible(false);
+				System.exit(0);
+			}
+		};
+	}
+
+	private Component buildMainPanel() {
+		JPanel mainPanel = new JPanel(new BorderLayout());
+		mainPanel.add(this.buildTaskListPanel(), BorderLayout.CENTER);
+		mainPanel.add(this.buildControlPanel(), BorderLayout.SOUTH);
+		return mainPanel;
+	}
+
+	private Component buildTaskListPanel() {
+		JPanel taskListPanel = new JPanel(new GridLayout(0, 1));
+		taskListPanel.add(this.buildPrimitiveTaskListPanel());
+		taskListPanel.add(this.buildDisplayableTaskListPanel());
+		return taskListPanel;
+	}
+
+	private Component buildPrimitiveTaskListPanel() {
+		JPanel taskListPanel = new JPanel(new GridLayout(1, 0));
+		taskListPanel.add(this.buildUnsortedPrimitiveListPanel());
+		taskListPanel.add(this.buildStandardSortedPrimitiveListPanel());
+		taskListPanel.add(this.buildCustomSortedPrimitiveListPanel());
+		return taskListPanel;
+	}
+
+	private Component buildDisplayableTaskListPanel() {
+		JPanel taskListPanel = new JPanel(new GridLayout(1, 0));
+		taskListPanel.add(this.buildUnsortedDisplayableListPanel());
+		taskListPanel.add(this.buildStandardSortedDisplayableListPanel());
+		taskListPanel.add(this.buildCustomSortedDisplayableListPanel());
+		return taskListPanel;
+	}
+
+	private Component buildUnsortedPrimitiveListPanel() {
+		return this.buildListPanel(" primitive unsorted", this.buildUnsortedPrimitiveListModel());
+	}
+
+	private Component buildStandardSortedPrimitiveListPanel() {
+		return this.buildListPanel(" primitive sorted", this.buildStandardSortedPrimitiveListModel());
+	}
+
+	private Component buildCustomSortedPrimitiveListPanel() {
+		return this.buildListPanel(" primitive reverse sorted", this.buildCustomSortedPrimitiveListModel());
+	}
+
+	private Component buildUnsortedDisplayableListPanel() {
+		return this.buildListPanel(" displayable unsorted", this.buildUnsortedDisplayableListModel());
+	}
+
+	private Component buildStandardSortedDisplayableListPanel() {
+		return this.buildListPanel(" displayable sorted", this.buildStandardSortedDisplayableListModel());
+	}
+
+	private Component buildCustomSortedDisplayableListPanel() {
+		return this.buildListPanel(" displayable reverse sorted", this.buildCustomSortedDisplayableListModel());
+	}
+
+	private ListModel buildUnsortedPrimitiveListModel() {
+		return new ListModelAdapter(this.buildPrimitiveTaskListAdapter());
+	}
+
+	private ListModel buildStandardSortedPrimitiveListModel() {
+		return new ListModelAdapter(new SortedListValueModelAdapter(this.buildPrimitiveTaskListAdapter()));
+	}
+
+	private ListModel buildCustomSortedPrimitiveListModel() {
+		return new ListModelAdapter(new SortedListValueModelAdapter(this.buildPrimitiveTaskListAdapter(), this.buildCustomComparator()));
+	}
+
+	private ListModel buildUnsortedDisplayableListModel() {
+		return new ListModelAdapter(this.buildDisplayableTaskListAdapter());
+	}
+
+	private ListModel buildStandardSortedDisplayableListModel() {
+		return new ListModelAdapter(new SortedListValueModelAdapter(this.buildDisplayableTaskListAdapter()));
+	}
+
+	private ListModel buildCustomSortedDisplayableListModel() {
+		return new ListModelAdapter(new SortedListValueModelAdapter(this.buildDisplayableTaskListAdapter(), this.buildCustomComparator()));
+	}
+
+	private Component buildListPanel(String label, ListModel listModel) {
+		JPanel listPanel = new JPanel(new BorderLayout());
+		JLabel listLabel = new JLabel(label);
+		listPanel.add(listLabel, BorderLayout.NORTH);
+
+		JList listBox = new JList();
+		listBox.setModel(listModel);
+		listBox.setDoubleBuffered(true);
+		listLabel.setLabelFor(listBox);
+		listPanel.add(new JScrollPane(listBox), BorderLayout.CENTER);
+		return listPanel;
+	}
+
+	private Comparator buildCustomComparator() {
+		return new Comparator() {
+			public int compare(Object o1, Object o2) {
+				return ((Comparable) o2).compareTo(o1);
+			}
+		};
+	}
+
+	private ListValueModel buildPrimitiveTaskListAdapter() {
+		return new ListAspectAdapter(TaskList.TASKS_LIST, this.taskList()) {
+			@Override
+			protected ListIterator getValueFromSubject() {
+				return ((TaskList) this.subject).tasks();
+			}
+		};
+	}
+
+	private ListValueModel buildDisplayableTaskListAdapter() {
+		return new ListAspectAdapter(TaskList.TASK_OBJECTS_LIST, this.taskList()) {
+			@Override
+			protected ListIterator getValueFromSubject() {
+				return ((TaskList) this.subject).taskObjects();
+			}
+		};
+	}
+
+	private Component buildControlPanel() {
+		JPanel controlPanel = new JPanel(new BorderLayout());
+		controlPanel.add(this.buildAddRemoveTaskPanel(), BorderLayout.CENTER);
+		controlPanel.add(this.buildClearButton(), BorderLayout.EAST);
+		return controlPanel;
+	}
+
+	private Component buildAddRemoveTaskPanel() {
+		JPanel addRemoveTaskPanel = new JPanel(new BorderLayout());
+		addRemoveTaskPanel.add(this.buildAddButton(), BorderLayout.WEST);
+		addRemoveTaskPanel.add(this.buildTaskTextField(), BorderLayout.CENTER);
+		addRemoveTaskPanel.add(this.buildRemoveButton(), BorderLayout.EAST);
+		return addRemoveTaskPanel;
+	}
+
+	private String getTask() {
+		return this.taskTextField.getText();
+	}
+
+	private TaskList taskList() {
+		return (TaskList) this.taskListHolder.getValue();
+	}
+
+	void addTask() {
+		String task = this.getTask();
+		if (task.length() != 0) {
+			this.taskList().addTask(task);
+		}
+	}
+
+	void removeTask() {
+		String task = this.getTask();
+		if (task.length() != 0) {
+			this.taskList().removeTask(task);
+		}
+	}
+
+	void clearTasks() {
+		this.taskList().clearTasks();
+	}
+
+	private TextField buildTaskTextField() {
+		this.taskTextField = new TextField();
+		return this.taskTextField;
+	}
+
+	private JButton buildAddButton() {
+		return new JButton(this.buildAddAction());
+	}
+
+	private Action buildAddAction() {
+		Action action = new AbstractAction("add") {
+			public void actionPerformed(ActionEvent event) {
+				ListModelAdapterUITest.this.addTask();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	private JButton buildRemoveButton() {
+		return new JButton(this.buildRemoveAction());
+	}
+
+	private Action buildRemoveAction() {
+		Action action = new AbstractAction("remove") {
+			public void actionPerformed(ActionEvent event) {
+				ListModelAdapterUITest.this.removeTask();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	private JButton buildClearButton() {
+		return new JButton(this.buildClearAction());
+	}
+
+	private Action buildClearAction() {
+		Action action = new AbstractAction("clear") {
+			public void actionPerformed(ActionEvent event) {
+				ListModelAdapterUITest.this.clearTasks();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	private class TaskList extends AbstractModel {
+		private List<String> tasks = new ArrayList<String>();
+		private List<TaskObject> taskObjects = new ArrayList<TaskObject>();
+		public static final String TASKS_LIST = "tasks";
+		public static final String TASK_OBJECTS_LIST = "taskObjects";
+		TaskList() {
+			super();
+		}
+		public ListIterator<String> tasks() {
+			return this.tasks.listIterator();
+		}
+		public ListIterator<TaskObject> taskObjects() {
+			return this.taskObjects.listIterator();
+		}
+		public void addTask(String task) {
+			int index = this.tasks.size();
+			this.tasks.add(index, task);
+			this.fireItemAdded(TASKS_LIST, index, task);
+	
+			TaskObject taskObject = new TaskObject(task);
+			this.taskObjects.add(index, taskObject);
+			this.fireItemAdded(TASK_OBJECTS_LIST, index, taskObject);
+		}		
+		public void removeTask(String task) {
+			int index = this.tasks.indexOf(task);
+			if (index != -1) {
+				Object removedTask = this.tasks.remove(index);
+				this.fireItemRemoved(TASKS_LIST, index, removedTask);
+				// assume the indexes match...
+				Object removedTaskObject = this.taskObjects.remove(index);
+				this.fireItemRemoved(TASK_OBJECTS_LIST, index, removedTaskObject);
+			}
+		}
+		public void clearTasks() {
+			this.tasks.clear();
+			this.fireListChanged(TASKS_LIST);
+			this.taskObjects.clear();
+			this.fireListChanged(TASK_OBJECTS_LIST);
+		}
+	}
+
+	private class TaskObject extends AbstractModel implements Displayable {
+		private String name;
+		private Date creationTimeStamp;
+		public TaskObject(String name) {
+			this.name = name;
+			this.creationTimeStamp = new Date();
+		}
+		public String displayString() {
+			return this.name + ": " + this.creationTimeStamp.getTime();
+		}
+		public Icon icon() {
+			return null;
+		}
+		public int compareTo(Displayable o) {
+			return DEFAULT_COMPARATOR.compare(this, o);
+		}
+		public String getName() {
+			return this.name;
+		}
+		public void setName(String name) {
+			Object old = this.name;
+			this.name = name;
+			this.firePropertyChanged(DISPLAY_STRING_PROPERTY, old, name);
+		}
+		@Override
+		public String toString() {
+			return "TaskObject(" + this.displayString() + ")";
+		}
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ListSpinnerModelAdapterTests.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ListSpinnerModelAdapterTests.java
new file mode 100644
index 0000000..9a715e5
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ListSpinnerModelAdapterTests.java
@@ -0,0 +1,129 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import javax.swing.SpinnerModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimplePropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+import org.eclipse.jpt.utility.internal.model.value.swing.ListSpinnerModelAdapter;
+import org.eclipse.jpt.utility.tests.internal.TestTools;
+
+import junit.framework.TestCase;
+
+public class ListSpinnerModelAdapterTests extends TestCase {
+	private PropertyValueModel valueHolder;
+	private SpinnerModel spinnerModelAdapter;
+	boolean eventFired;
+	private static final String[] VALUE_LIST = {"red", "green", "blue"};
+	private static final String DEFAULT_VALUE = VALUE_LIST[0];
+
+	public ListSpinnerModelAdapterTests(String name) {
+		super(name);
+	}
+
+	@Override
+	protected void setUp() throws Exception {
+		super.setUp();
+		this.valueHolder = new SimplePropertyValueModel(DEFAULT_VALUE);
+		this.spinnerModelAdapter = new ListSpinnerModelAdapter(this.valueHolder, VALUE_LIST);
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		TestTools.clear(this);
+		super.tearDown();
+	}
+
+	public void testSetValueSpinnerModel() throws Exception {
+		this.eventFired = false;
+		this.spinnerModelAdapter.addChangeListener(new TestChangeListener() {
+			@Override
+			public void stateChanged(ChangeEvent e) {
+				ListSpinnerModelAdapterTests.this.eventFired = true;
+			}
+		});
+		assertEquals(DEFAULT_VALUE, this.valueHolder.getValue());
+		this.spinnerModelAdapter.setValue(VALUE_LIST[2]);
+		assertTrue(this.eventFired);
+		assertEquals(VALUE_LIST[2], this.valueHolder.getValue());
+	}
+
+	public void testSetValueValueHolder() throws Exception {
+		this.eventFired = false;
+		this.spinnerModelAdapter.addChangeListener(new TestChangeListener() {
+			@Override
+			public void stateChanged(ChangeEvent e) {
+				ListSpinnerModelAdapterTests.this.eventFired = true;
+			}
+		});
+		assertEquals(DEFAULT_VALUE, this.spinnerModelAdapter.getValue());
+		this.valueHolder.setValue(VALUE_LIST[2]);
+		assertTrue(this.eventFired);
+		assertEquals(VALUE_LIST[2], this.spinnerModelAdapter.getValue());
+	}
+
+	public void testDefaultValue() throws Exception {
+		this.eventFired = false;
+		this.spinnerModelAdapter.addChangeListener(new TestChangeListener() {
+			@Override
+			public void stateChanged(ChangeEvent e) {
+				ListSpinnerModelAdapterTests.this.eventFired = true;
+			}
+		});
+		assertEquals(DEFAULT_VALUE, this.spinnerModelAdapter.getValue());
+
+		this.valueHolder.setValue(VALUE_LIST[2]);
+		assertTrue(this.eventFired);
+		assertEquals(VALUE_LIST[2], this.spinnerModelAdapter.getValue());
+
+		this.eventFired = false;
+		this.valueHolder.setValue(null);
+		assertTrue(this.eventFired);
+		assertEquals(VALUE_LIST[0], this.spinnerModelAdapter.getValue());
+	}
+
+	public void testHasListeners() throws Exception {
+		SimplePropertyValueModel localValueHolder = (SimplePropertyValueModel) this.valueHolder;
+		assertFalse(localValueHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasNoListeners(this.spinnerModelAdapter);
+
+		ChangeListener listener = new TestChangeListener();
+		this.spinnerModelAdapter.addChangeListener(listener);
+		assertTrue(localValueHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasListeners(this.spinnerModelAdapter);
+
+		this.spinnerModelAdapter.removeChangeListener(listener);
+		assertFalse(localValueHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasNoListeners(this.spinnerModelAdapter);
+	}
+
+	private void verifyHasNoListeners(SpinnerModel adapter) throws Exception {
+		assertEquals(0, ((ListSpinnerModelAdapter) adapter).getChangeListeners().length);
+	}
+
+	private void verifyHasListeners(Object adapter) throws Exception {
+		assertFalse(((ListSpinnerModelAdapter) adapter).getChangeListeners().length == 0);
+	}
+
+
+	private class TestChangeListener implements ChangeListener {
+		TestChangeListener() {
+			super();
+		}
+		public void stateChanged(ChangeEvent e) {
+			fail("unexpected event");
+		}
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/NumberSpinnerModelAdapterTests.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/NumberSpinnerModelAdapterTests.java
new file mode 100644
index 0000000..0077539
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/NumberSpinnerModelAdapterTests.java
@@ -0,0 +1,138 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import javax.swing.SpinnerModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimplePropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+import org.eclipse.jpt.utility.internal.model.value.swing.NumberSpinnerModelAdapter;
+import org.eclipse.jpt.utility.tests.internal.TestTools;
+
+import junit.framework.TestCase;
+
+public class NumberSpinnerModelAdapterTests extends TestCase {
+	private PropertyValueModel valueHolder;
+	private SpinnerModel spinnerModelAdapter;
+	boolean eventFired;
+
+	public NumberSpinnerModelAdapterTests(String name) {
+		super(name);
+	}
+
+	@Override
+	protected void setUp() throws Exception {
+		super.setUp();
+		this.valueHolder = new SimplePropertyValueModel(new Integer(0));
+		this.spinnerModelAdapter = new NumberSpinnerModelAdapter(this.valueHolder, -33, 33, 1);
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		TestTools.clear(this);
+		super.tearDown();
+	}
+
+	public void testSetValueSpinnerModel() throws Exception {
+		this.eventFired = false;
+		this.spinnerModelAdapter.addChangeListener(new TestChangeListener() {
+			@Override
+			public void stateChanged(ChangeEvent e) {
+				NumberSpinnerModelAdapterTests.this.eventFired = true;
+			}
+		});
+		this.spinnerModelAdapter.setValue(new Integer(5));
+		assertTrue(this.eventFired);
+		assertEquals(new Integer(5), this.valueHolder.getValue());
+	}
+
+	public void testSetValueValueHolder() throws Exception {
+		this.eventFired = false;
+		this.spinnerModelAdapter.addChangeListener(new TestChangeListener() {
+			@Override
+			public void stateChanged(ChangeEvent e) {
+				NumberSpinnerModelAdapterTests.this.eventFired = true;
+			}
+		});
+		assertEquals(new Integer(0), this.spinnerModelAdapter.getValue());
+		this.valueHolder.setValue(new Integer(7));
+		assertTrue(this.eventFired);
+		assertEquals(new Integer(7), this.spinnerModelAdapter.getValue());
+	}
+
+	public void testDefaultValue() throws Exception {
+		this.eventFired = false;
+		this.spinnerModelAdapter.addChangeListener(new TestChangeListener() {
+			@Override
+			public void stateChanged(ChangeEvent e) {
+				NumberSpinnerModelAdapterTests.this.eventFired = true;
+			}
+		});
+		assertEquals(new Integer(0), this.spinnerModelAdapter.getValue());
+		this.valueHolder.setValue(null);
+		assertTrue(this.eventFired);
+		assertEquals(new Integer(-33), this.spinnerModelAdapter.getValue());
+	}
+
+	public void testHasListeners() throws Exception {
+		SimplePropertyValueModel localValueHolder = (SimplePropertyValueModel) this.valueHolder;
+		assertFalse(localValueHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasNoListeners(this.spinnerModelAdapter);
+
+		ChangeListener listener = new TestChangeListener();
+		this.spinnerModelAdapter.addChangeListener(listener);
+		assertTrue(localValueHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasListeners(this.spinnerModelAdapter);
+
+		this.spinnerModelAdapter.removeChangeListener(listener);
+		assertFalse(localValueHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasNoListeners(this.spinnerModelAdapter);
+	}
+
+	private void verifyHasNoListeners(SpinnerModel adapter) throws Exception {
+		assertEquals(0, ((NumberSpinnerModelAdapter) adapter).getChangeListeners().length);
+	}
+
+	private void verifyHasListeners(Object adapter) throws Exception {
+		assertFalse(((NumberSpinnerModelAdapter) adapter).getChangeListeners().length == 0);
+	}
+
+	public void testNullInitialValue() {
+		this.valueHolder = new SimplePropertyValueModel();
+		this.spinnerModelAdapter = new NumberSpinnerModelAdapter(this.valueHolder, new Integer(-33), new Integer(33), new Integer(1), new Integer(0));
+
+		this.eventFired = false;
+		this.spinnerModelAdapter.addChangeListener(new TestChangeListener() {
+			@Override
+			public void stateChanged(ChangeEvent e) {
+				NumberSpinnerModelAdapterTests.this.eventFired = true;
+			}
+		});
+		assertEquals(new Integer(0), this.spinnerModelAdapter.getValue());
+		this.valueHolder.setValue(new Integer(7));
+		assertTrue(this.eventFired);
+		assertEquals(new Integer(7), this.spinnerModelAdapter.getValue());
+	}
+
+
+	// ********** inner class **********
+	private class TestChangeListener implements ChangeListener {
+		TestChangeListener() {
+			super();
+		}
+		public void stateChanged(ChangeEvent e) {
+			fail("unexpected event");
+		}
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ObjectListSelectionModelTests.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ObjectListSelectionModelTests.java
new file mode 100644
index 0000000..13aea76
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ObjectListSelectionModelTests.java
@@ -0,0 +1,204 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import javax.swing.DefaultListModel;
+import javax.swing.ListModel;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+
+import org.eclipse.jpt.utility.internal.CollectionTools;
+import org.eclipse.jpt.utility.internal.model.value.swing.ObjectListSelectionModel;
+import org.eclipse.jpt.utility.tests.internal.TestTools;
+
+import junit.framework.TestCase;
+
+public class ObjectListSelectionModelTests extends TestCase {
+	private DefaultListModel listModel;
+	private ObjectListSelectionModel selectionModel;
+
+	public ObjectListSelectionModelTests(String name) {
+		super(name);
+	}
+
+	@Override
+	protected void setUp() throws Exception {
+		super.setUp();
+		this.listModel = this.buildListModel();
+		this.selectionModel = this.buildSelectionModel(this.listModel);
+	}
+
+	private DefaultListModel buildListModel() {
+		DefaultListModel lm = new DefaultListModel();
+		lm.addElement("foo");
+		lm.addElement("bar");
+		lm.addElement("baz");
+		return lm;
+	}
+
+	private ObjectListSelectionModel buildSelectionModel(ListModel lm) {
+		return new ObjectListSelectionModel(lm);
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		TestTools.clear(this);
+		super.tearDown();
+	}
+
+	public void testListDataListener() {
+		this.selectionModel.addListSelectionListener(this.buildListSelectionListener());
+		this.selectionModel.setSelectionInterval(0, 0);
+		assertEquals("foo", this.selectionModel.getSelectedValue());
+		this.listModel.set(0, "jar");
+		assertEquals("jar", this.selectionModel.getSelectedValue());
+	}
+
+	public void testGetSelectedValue() {
+		this.selectionModel.setSelectionInterval(0, 0);
+		assertEquals("foo", this.selectionModel.getSelectedValue());
+	}
+
+	public void testGetSelectedValues() {
+		this.selectionModel.setSelectionInterval(0, 0);
+		this.selectionModel.addSelectionInterval(2, 2);
+		assertEquals(2, this.selectionModel.getSelectedValues().length);
+		assertTrue(CollectionTools.contains(this.selectionModel.getSelectedValues(), "foo"));
+		assertTrue(CollectionTools.contains(this.selectionModel.getSelectedValues(), "baz"));
+	}
+
+	public void testSetSelectedValue() {
+		this.selectionModel.setSelectedValue("foo");
+		assertEquals(0, this.selectionModel.getMinSelectionIndex());
+		assertEquals(0, this.selectionModel.getMaxSelectionIndex());
+	}
+
+	public void testSetSelectedValues() {
+		this.selectionModel.setSelectedValues(new Object[] {"foo", "baz"});
+		assertEquals(0, this.selectionModel.getMinSelectionIndex());
+		assertEquals(2, this.selectionModel.getMaxSelectionIndex());
+	}
+
+	public void testAddSelectedValue() {
+		this.listModel.addElement("joo");
+		this.listModel.addElement("jar");
+		this.listModel.addElement("jaz");
+		this.selectionModel.setSelectedValue("foo");
+		this.selectionModel.addSelectedValue("jaz");
+		assertEquals(0, this.selectionModel.getMinSelectionIndex());
+		assertEquals(5, this.selectionModel.getMaxSelectionIndex());
+		assertTrue(this.selectionModel.isSelectedIndex(0));
+		assertFalse(this.selectionModel.isSelectedIndex(1));
+		assertFalse(this.selectionModel.isSelectedIndex(2));
+		assertFalse(this.selectionModel.isSelectedIndex(3));
+		assertFalse(this.selectionModel.isSelectedIndex(4));
+		assertTrue(this.selectionModel.isSelectedIndex(5));
+	}
+
+	public void testAddSelectedValues() {
+		this.listModel.addElement("joo");
+		this.listModel.addElement("jar");
+		this.listModel.addElement("jaz");
+		this.selectionModel.setSelectedValue("foo");
+		this.selectionModel.addSelectedValues(new Object[] {"bar", "jar"});
+		assertEquals(0, this.selectionModel.getMinSelectionIndex());
+		assertEquals(4, this.selectionModel.getMaxSelectionIndex());
+		assertTrue(this.selectionModel.isSelectedIndex(0));
+		assertTrue(this.selectionModel.isSelectedIndex(1));
+		assertFalse(this.selectionModel.isSelectedIndex(2));
+		assertFalse(this.selectionModel.isSelectedIndex(3));
+		assertTrue(this.selectionModel.isSelectedIndex(4));
+		assertFalse(this.selectionModel.isSelectedIndex(5));
+	}
+
+	public void testRemoveSelectedValue() {
+		this.listModel.addElement("joo");
+		this.listModel.addElement("jar");
+		this.listModel.addElement("jaz");
+		this.selectionModel.setSelectedValues(new Object[] {"foo", "baz", "jar"});
+		this.selectionModel.removeSelectedValue("jar");
+		assertEquals(0, this.selectionModel.getMinSelectionIndex());
+		assertEquals(2, this.selectionModel.getMaxSelectionIndex());
+		assertTrue(this.selectionModel.isSelectedIndex(0));
+		assertFalse(this.selectionModel.isSelectedIndex(1));
+		assertTrue(this.selectionModel.isSelectedIndex(2));
+		assertFalse(this.selectionModel.isSelectedIndex(3));
+		assertFalse(this.selectionModel.isSelectedIndex(4));
+		assertFalse(this.selectionModel.isSelectedIndex(5));
+	}
+
+	public void testRemoveSelectedValues() {
+		this.listModel.addElement("joo");
+		this.listModel.addElement("jar");
+		this.listModel.addElement("jaz");
+		this.selectionModel.setSelectedValues(new Object[] {"foo", "baz", "joo", "jar"});
+		this.selectionModel.removeSelectedValues(new Object[] {"foo", "joo"});
+		assertEquals(2, this.selectionModel.getMinSelectionIndex());
+		assertEquals(4, this.selectionModel.getMaxSelectionIndex());
+		assertFalse(this.selectionModel.isSelectedIndex(0));
+		assertFalse(this.selectionModel.isSelectedIndex(1));
+		assertTrue(this.selectionModel.isSelectedIndex(2));
+		assertFalse(this.selectionModel.isSelectedIndex(3));
+		assertTrue(this.selectionModel.isSelectedIndex(4));
+		assertFalse(this.selectionModel.isSelectedIndex(5));
+	}
+
+	public void testGetAnchorSelectedValue() {
+		this.selectionModel.setAnchorSelectionIndex(1);
+		assertEquals("bar", this.selectionModel.getAnchorSelectedValue());
+	}
+
+	public void testGetLeadSelectedValue() {
+		this.selectionModel.setSelectedValue("bar");
+		assertEquals("bar", this.selectionModel.getLeadSelectedValue());
+		this.selectionModel.setSelectedValues(new Object[] {"foo", "baz"});
+		assertEquals("baz", this.selectionModel.getLeadSelectedValue());
+	}
+
+	public void testGetMinMaxSelectedValue() {
+		this.listModel.addElement("joo");
+		this.listModel.addElement("jar");
+		this.listModel.addElement("jaz");
+		this.selectionModel.setSelectedValue("foo");
+		this.selectionModel.addSelectedValues(new Object[] {"bar", "jar"});
+		assertEquals("foo", this.selectionModel.getMinSelectedValue());
+		assertEquals("jar", this.selectionModel.getMaxSelectedValue());
+	}
+
+	public void testValueIsSelected() {
+		this.listModel.addElement("joo");
+		this.listModel.addElement("jar");
+		this.listModel.addElement("jaz");
+		this.selectionModel.setSelectedValue("foo");
+		this.selectionModel.addSelectedValues(new Object[] {"bar", "jar"});
+		assertTrue(this.selectionModel.valueIsSelected("foo"));
+		assertTrue(this.selectionModel.valueIsSelected("bar"));
+		assertTrue(this.selectionModel.valueIsSelected("jar"));
+		assertFalse(this.selectionModel.valueIsSelected("baz"));
+	}
+
+	public void testHasListeners() throws Exception {
+		ListSelectionListener listener = this.buildListSelectionListener();
+		assertEquals(0, this.listModel.getListDataListeners().length);
+		this.selectionModel.addListSelectionListener(listener);
+		assertEquals(1, this.listModel.getListDataListeners().length);
+		this.selectionModel.removeListSelectionListener(listener);
+		assertEquals(0, this.listModel.getListDataListeners().length);
+	}
+
+	private ListSelectionListener buildListSelectionListener() {
+		return new ListSelectionListener() {
+			public void valueChanged(ListSelectionEvent e) {
+				// do nothing for now...
+			}
+		};
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/PrimitiveListTreeModelTests.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/PrimitiveListTreeModelTests.java
new file mode 100644
index 0000000..12d1e06
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/PrimitiveListTreeModelTests.java
@@ -0,0 +1,215 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.ListIterator;
+
+import javax.swing.event.TreeModelEvent;
+import javax.swing.event.TreeModelListener;
+import javax.swing.tree.DefaultMutableTreeNode;
+import javax.swing.tree.TreeModel;
+
+import org.eclipse.jpt.utility.internal.iterators.ReadOnlyListIterator;
+import org.eclipse.jpt.utility.internal.model.AbstractModel;
+import org.eclipse.jpt.utility.internal.model.value.ListAspectAdapter;
+import org.eclipse.jpt.utility.internal.model.value.ListValueModel;
+import org.eclipse.jpt.utility.internal.model.value.swing.PrimitiveListTreeModel;
+import org.eclipse.jpt.utility.tests.internal.TestTools;
+
+import junit.framework.TestCase;
+
+public class PrimitiveListTreeModelTests extends TestCase {
+	TestModel testModel;
+	private TreeModel treeModel;
+
+	public PrimitiveListTreeModelTests(String name) {
+		super(name);
+	}
+
+	@Override
+	protected void setUp() throws Exception {
+		super.setUp();
+		this.testModel = this.buildTestModel();
+		this.treeModel = this.buildTreeModel();
+	}
+
+	private TestModel buildTestModel() {
+		return new TestModel();
+	}
+
+	private TreeModel buildTreeModel() {
+		return new PrimitiveListTreeModel(this.buildListValueModel()) {
+			@Override
+			protected void primitiveChanged(int index, Object newValue) {
+				if ( ! newValue.equals("")) {
+					PrimitiveListTreeModelTests.this.testModel.replaceName(index, (String) newValue);
+				}
+			}
+		};
+	}
+
+	private ListValueModel buildListValueModel() {
+		return new ListAspectAdapter(TestModel.NAMES_LIST, this.testModel) {
+			@Override
+			protected ListIterator getValueFromSubject() {
+				return ((TestModel) this.subject).names();
+			}
+			@Override
+			public Object getItem(int index) {
+				return ((TestModel) this.subject).getName(index);
+			}
+			@Override
+			public int size() {
+				return ((TestModel) this.subject).namesSize();
+			}
+			@Override
+			public void addItem(int index, Object item) {
+				((TestModel) this.subject).addName(index, (String) item);
+			}
+			@Override
+			public void addItems(int index, List items) {
+				((TestModel) this.subject).addNames(index, items);
+			}
+			@Override
+			public Object removeItem(int index) {
+				return ((TestModel) this.subject).removeName(index);
+			}
+			@Override
+			public List removeItems(int index, int length) {
+				return ((TestModel) this.subject).removeNames(index, length);
+			}
+			@Override
+			public Object replaceItem(int index, Object item) {
+				return ((TestModel) this.subject).replaceName(index, (String) item);
+			}
+		};
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		TestTools.clear(this);
+		super.tearDown();
+	}
+
+	public void testAddPrimitive() {
+		this.treeModel.addTreeModelListener(new TestTreeModelListener() {
+			@Override
+			public void treeNodesInserted(TreeModelEvent e) {
+				PrimitiveListTreeModelTests.this.verifyTreeModelEvent(e, new int[] {0}, new String[] {"foo"});
+			}
+		});
+		this.testModel.addName("foo");
+	}
+
+	public void testRemovePrimitive() {
+		this.testModel.addName("foo");
+		this.testModel.addName("bar");
+		this.testModel.addName("baz");
+		this.treeModel.addTreeModelListener(new TestTreeModelListener() {
+			@Override
+			public void treeNodesRemoved(TreeModelEvent e) {
+				PrimitiveListTreeModelTests.this.verifyTreeModelEvent(e, new int[] {1}, new String[] {"bar"});
+			}
+		});
+		String name = this.testModel.removeName(1);
+		assertEquals("bar", name);
+	}
+
+	public void testReplacePrimitive() {
+		this.testModel.addName("foo");
+		this.testModel.addName("bar");
+		this.testModel.addName("baz");
+		this.treeModel.addTreeModelListener(new TestTreeModelListener() {
+			@Override
+			public void treeNodesChanged(TreeModelEvent e) {
+				PrimitiveListTreeModelTests.this.verifyTreeModelEvent(e, new int[] {1}, new String[] {"jar"});
+			}
+		});
+		String name = this.testModel.replaceName(1, "jar");
+		assertEquals("bar", name);
+	}
+
+	void verifyTreeModelEvent(TreeModelEvent e, int[] expectedChildIndices, String[] expectedNames) {
+		assertTrue(Arrays.equals(expectedChildIndices, e.getChildIndices()));
+		Object[] actualChildren = e.getChildren();
+		assertEquals(expectedNames.length, actualChildren.length);
+		for (int i = 0; i < expectedNames.length; i++) {
+			DefaultMutableTreeNode node = (DefaultMutableTreeNode) actualChildren[i];
+			assertEquals(expectedNames[i], node.getUserObject());
+		}
+		assertEquals(1, e.getPath().length);
+		assertEquals(this.treeModel.getRoot(), e.getPath()[0]);
+		assertEquals(this.treeModel, e.getSource());
+	}
+
+
+// ********** inner classes **********
+
+	private class TestModel extends AbstractModel {
+		private final List<String> names;
+			static final String NAMES_LIST = "names";
+	
+		TestModel() {
+			super();
+			this.names = new ArrayList<String>();
+		}
+	
+		public ListIterator<String> names() {
+			return new ReadOnlyListIterator<String>(this.names);
+		}
+		public int namesSize() {
+			return this.names.size();
+		}
+		public String getName(int index) {
+			return this.names.get(index);
+		}
+		public void addName(int index, String name) {
+			this.addItemToList(index, name, this.names, NAMES_LIST);
+		}
+		public void addName(String name) {
+			this.addName(this.namesSize(), name);
+		}
+		public void addNames(int index, List list) {
+			this.addItemsToList(index, this.names, list, NAMES_LIST);
+		}
+		public void addNames(List list) {
+			this.addNames(this.namesSize(), list);
+		}
+		public String removeName(int index) {
+			return (String) this.removeItemFromList(index, this.names, NAMES_LIST);
+		}
+		public List removeNames(int index, int length) {
+			return this.removeItemsFromList(index, length, this.names, NAMES_LIST);
+		}
+		public String replaceName(int index, String newName) {
+			return this.setItemInList(index, newName, this.names, NAMES_LIST);
+		}
+	}
+	
+	
+	public class TestTreeModelListener implements TreeModelListener {
+		public void treeNodesChanged(TreeModelEvent e) {
+			fail("unexpected event");
+		}
+		public void treeNodesInserted(TreeModelEvent e) {
+			fail("unexpected event");
+		}
+		public void treeNodesRemoved(TreeModelEvent e) {
+			fail("unexpected event");
+		}
+		public void treeStructureChanged(TreeModelEvent e) {
+			fail("unexpected event");
+		}
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/RadioButtonModelAdapterTests.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/RadioButtonModelAdapterTests.java
new file mode 100644
index 0000000..dc6791e
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/RadioButtonModelAdapterTests.java
@@ -0,0 +1,221 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import javax.swing.ButtonModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.EventListenerList;
+
+import org.eclipse.jpt.utility.internal.ClassTools;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimplePropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+import org.eclipse.jpt.utility.internal.model.value.swing.RadioButtonModelAdapter;
+import org.eclipse.jpt.utility.tests.internal.TestTools;
+
+import junit.framework.TestCase;
+
+public class RadioButtonModelAdapterTests extends TestCase {
+	private PropertyValueModel valueHolder;
+
+	private ButtonModel redButtonModelAdapter;
+	private ChangeListener redListener;
+	boolean redEventFired;
+
+	private ButtonModel greenButtonModelAdapter;
+	private ChangeListener greenListener;
+	boolean greenEventFired;
+
+	private ButtonModel blueButtonModelAdapter;
+	private ChangeListener blueListener;
+	boolean blueEventFired;
+
+//	private ButtonGroup buttonGroup;	// DO NOT use a ButtonGroup
+
+	private static final String RED = "red";
+	private static final String GREEN = "green";
+	private static final String BLUE = "blue";
+
+	public RadioButtonModelAdapterTests(String name) {
+		super(name);
+	}
+
+	@Override
+	protected void setUp() throws Exception {
+		super.setUp();
+		this.valueHolder = new SimplePropertyValueModel(null);
+//		buttonGroup = new ButtonGroup();
+
+		this.redButtonModelAdapter = new RadioButtonModelAdapter(this.valueHolder, RED);
+//		this.redButtonModelAdapter.setGroup(buttonGroup);
+		this.redListener = new TestChangeListener() {
+			@Override
+			public void stateChanged(ChangeEvent e) {
+				RadioButtonModelAdapterTests.this.redEventFired = true;
+			}
+		};
+
+		this.greenButtonModelAdapter = new RadioButtonModelAdapter(this.valueHolder, GREEN);
+//		this.greenButtonModelAdapter.setGroup(buttonGroup);
+		this.greenListener = new TestChangeListener() {
+			@Override
+			public void stateChanged(ChangeEvent e) {
+				RadioButtonModelAdapterTests.this.greenEventFired = true;
+			}
+		};
+
+		this.blueButtonModelAdapter = new RadioButtonModelAdapter(this.valueHolder, BLUE);
+//		this.blueButtonModelAdapter.setGroup(buttonGroup);
+		this.blueListener = new TestChangeListener() {
+			@Override
+			public void stateChanged(ChangeEvent e) {
+				RadioButtonModelAdapterTests.this.blueEventFired = true;
+			}
+		};
+		
+		this.clearFlags();
+	}
+
+	private void listenToModelAdapters() {
+		this.redButtonModelAdapter.addChangeListener(this.redListener);
+		this.greenButtonModelAdapter.addChangeListener(this.greenListener);
+		this.blueButtonModelAdapter.addChangeListener(this.blueListener);
+	}
+
+	private void clearFlags() {
+		this.redEventFired = false;
+		this.greenEventFired = false;
+		this.blueEventFired = false;
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		TestTools.clear(this);
+		super.tearDown();
+	}
+
+	public void testSetSelected() throws Exception {
+		this.listenToModelAdapters();
+
+		this.greenButtonModelAdapter.setSelected(true);
+		assertFalse(this.redEventFired);
+		assertTrue(this.greenEventFired);
+		assertFalse(this.blueEventFired);
+		assertEquals(GREEN, this.valueHolder.getValue());
+
+		this.clearFlags();
+		this.blueButtonModelAdapter.setSelected(true);
+		assertFalse(this.redEventFired);
+		assertTrue(this.greenEventFired);
+		assertTrue(this.blueEventFired);
+		assertEquals(BLUE, this.valueHolder.getValue());
+
+		this.clearFlags();
+		this.redButtonModelAdapter.setSelected(true);
+		assertTrue(this.redEventFired);
+		assertFalse(this.greenEventFired);
+		assertTrue(this.blueEventFired);
+		assertEquals(RED, this.valueHolder.getValue());
+	}
+
+	public void testSetValue() throws Exception {
+		this.listenToModelAdapters();
+
+		this.greenButtonModelAdapter.setSelected(true);
+
+		this.clearFlags();
+		this.valueHolder.setValue(BLUE);
+		assertFalse(this.redEventFired);
+		assertTrue(this.greenEventFired);
+		assertTrue(this.blueEventFired);
+		assertFalse(this.redButtonModelAdapter.isSelected());
+		assertFalse(this.greenButtonModelAdapter.isSelected());
+		assertTrue(this.blueButtonModelAdapter.isSelected());
+
+		this.clearFlags();
+		this.valueHolder.setValue(RED);
+		assertTrue(this.redEventFired);
+		assertFalse(this.greenEventFired);
+		assertTrue(this.blueEventFired);
+		assertTrue(this.redButtonModelAdapter.isSelected());
+		assertFalse(this.greenButtonModelAdapter.isSelected());
+		assertFalse(this.blueButtonModelAdapter.isSelected());
+	}
+
+	public void testDefaultValue() throws Exception {
+		this.listenToModelAdapters();
+
+		this.valueHolder.setValue(GREEN);
+		assertFalse(this.redButtonModelAdapter.isSelected());
+		assertTrue(this.greenButtonModelAdapter.isSelected());
+		assertFalse(this.blueButtonModelAdapter.isSelected());
+
+		this.clearFlags();
+		this.valueHolder.setValue(null);
+		assertFalse(this.redEventFired);
+		assertTrue(this.greenEventFired);
+		assertFalse(this.blueEventFired);
+		assertFalse(this.redButtonModelAdapter.isSelected());
+		assertFalse(this.greenButtonModelAdapter.isSelected());
+		assertFalse(this.blueButtonModelAdapter.isSelected());
+
+		this.clearFlags();
+		this.valueHolder.setValue(BLUE);
+		assertFalse(this.redEventFired);
+		assertFalse(this.greenEventFired);
+		assertTrue(this.blueEventFired);
+		assertFalse(this.redButtonModelAdapter.isSelected());
+		assertFalse(this.greenButtonModelAdapter.isSelected());
+		assertTrue(this.blueButtonModelAdapter.isSelected());
+	}
+
+	public void testHasListeners() throws Exception {
+		SimplePropertyValueModel localValueHolder = (SimplePropertyValueModel) this.valueHolder;
+		assertFalse(localValueHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasNoListeners(this.redButtonModelAdapter);
+		this.verifyHasNoListeners(this.greenButtonModelAdapter);
+		this.verifyHasNoListeners(this.blueButtonModelAdapter);
+
+		ChangeListener listener = new TestChangeListener();
+		this.redButtonModelAdapter.addChangeListener(listener);
+		assertTrue(localValueHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasListeners(this.redButtonModelAdapter);
+		this.verifyHasNoListeners(this.greenButtonModelAdapter);
+		this.verifyHasNoListeners(this.blueButtonModelAdapter);
+
+		this.redButtonModelAdapter.removeChangeListener(listener);
+		assertFalse(localValueHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasNoListeners(this.redButtonModelAdapter);
+		this.verifyHasNoListeners(this.greenButtonModelAdapter);
+		this.verifyHasNoListeners(this.blueButtonModelAdapter);
+	}
+
+	private void verifyHasNoListeners(Object model) throws Exception {
+		EventListenerList listenerList = (EventListenerList) ClassTools.getFieldValue(model, "listenerList");
+		assertEquals(0, listenerList.getListenerList().length);
+	}
+
+	private void verifyHasListeners(Object model) throws Exception {
+		EventListenerList listenerList = (EventListenerList) ClassTools.getFieldValue(model, "listenerList");
+		assertFalse(listenerList.getListenerList().length == 0);
+	}
+
+
+	private class TestChangeListener implements ChangeListener {
+		TestChangeListener() {
+			super();
+		}
+		public void stateChanged(ChangeEvent e) {
+			fail("unexpected event");
+		}
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/RadioButtonModelAdapterUITest.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/RadioButtonModelAdapterUITest.java
new file mode 100644
index 0000000..39074f2
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/RadioButtonModelAdapterUITest.java
@@ -0,0 +1,259 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.ButtonModel;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.WindowConstants;
+
+import org.eclipse.jpt.utility.internal.CollectionTools;
+import org.eclipse.jpt.utility.internal.model.AbstractModel;
+import org.eclipse.jpt.utility.internal.model.value.PropertyAspectAdapter;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimplePropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+import org.eclipse.jpt.utility.internal.model.value.swing.RadioButtonModelAdapter;
+
+
+/**
+ * Play around with a set of radio buttons.
+ */
+public class RadioButtonModelAdapterUITest {
+
+	private TestModel testModel;
+	private PropertyValueModel testModelHolder;
+	private PropertyValueModel colorHolder;
+	private ButtonModel redButtonModel;
+	private ButtonModel greenButtonModel;
+	private ButtonModel blueButtonModel;
+
+	public static void main(String[] args) throws Exception {
+		new RadioButtonModelAdapterUITest().exec(args);
+	}
+
+	private RadioButtonModelAdapterUITest() {
+		super();
+	}
+
+	private void exec(String[] args) throws Exception {
+		this.testModel = new TestModel();
+		this.testModelHolder = new SimplePropertyValueModel(this.testModel);
+		this.colorHolder = this.buildColorHolder(this.testModelHolder);
+		this.redButtonModel = this.buildRadioButtonModelAdapter(this.colorHolder, TestModel.RED);
+		this.greenButtonModel = this.buildRadioButtonModelAdapter(this.colorHolder, TestModel.GREEN);
+		this.blueButtonModel = this.buildRadioButtonModelAdapter(this.colorHolder, TestModel.BLUE);
+		this.openWindow();
+	}
+
+	private PropertyValueModel buildColorHolder(ValueModel subjectHolder) {
+		return new PropertyAspectAdapter(subjectHolder, TestModel.COLOR_PROPERTY) {
+			@Override
+			protected Object getValueFromSubject() {
+				return ((TestModel) this.subject).getColor();
+			}
+			@Override
+			protected void setValueOnSubject(Object value) {
+				((TestModel) this.subject).setColor((String) value);
+			}
+		};
+	}
+
+	private ButtonModel buildRadioButtonModelAdapter(PropertyValueModel colorPVM, String color) {
+		return new RadioButtonModelAdapter(colorPVM, color);
+	}
+
+	private void openWindow() {
+		JFrame window = new JFrame(this.getClass().getName());
+		window.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
+		window.addWindowListener(this.buildWindowListener());
+		window.getContentPane().add(this.buildMainPanel(), "Center");
+		window.setSize(400, 100);
+		window.setLocation(200, 200);
+		window.setVisible(true);
+	}
+
+	private WindowListener buildWindowListener() {
+		return new WindowAdapter() {
+			@Override
+			public void windowClosing(WindowEvent e) {
+				e.getWindow().setVisible(false);
+				System.exit(0);
+			}
+		};
+	}
+
+	private Component buildMainPanel() {
+		JPanel mainPanel = new JPanel(new BorderLayout());
+		mainPanel.add(this.buildRadioButtonPanel(), BorderLayout.NORTH);
+		mainPanel.add(this.buildControlPanel(), BorderLayout.SOUTH);
+		return mainPanel;
+	}
+
+	private Component buildRadioButtonPanel() {
+		JPanel taskListPanel = new JPanel(new GridLayout(1, 0));
+		taskListPanel.add(this.buildRedRadioButton());
+		taskListPanel.add(this.buildGreenRadioButton());
+		taskListPanel.add(this.buildBlueRadioButton());
+		return taskListPanel;
+	}
+
+	private JRadioButton buildRedRadioButton() {
+		JRadioButton radioButton = new JRadioButton();
+		radioButton.setText("red");
+		radioButton.setModel(this.redButtonModel);
+		return radioButton;
+	}
+
+	private JRadioButton buildGreenRadioButton() {
+		JRadioButton radioButton = new JRadioButton();
+		radioButton.setText("green");
+		radioButton.setModel(this.greenButtonModel);
+		return radioButton;
+	}
+
+	private JRadioButton buildBlueRadioButton() {
+		JRadioButton radioButton = new JRadioButton();
+		radioButton.setText("blue");
+		radioButton.setModel(this.blueButtonModel);
+		return radioButton;
+	}
+
+	private Component buildControlPanel() {
+		JPanel controlPanel = new JPanel(new GridLayout(1, 0));
+		controlPanel.add(this.buildResetColorButton());
+		controlPanel.add(this.buildClearModelButton());
+		controlPanel.add(this.buildRestoreModelButton());
+		controlPanel.add(this.buildPrintModelButton());
+		return controlPanel;
+	}
+
+	private JButton buildResetColorButton() {
+		return new JButton(this.buildResetColorAction());
+	}
+
+	private Action buildResetColorAction() {
+		Action action = new AbstractAction("reset color") {
+			public void actionPerformed(ActionEvent event) {
+				RadioButtonModelAdapterUITest.this.resetColor();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void resetColor() {
+		this.testModel.setColor(TestModel.DEFAULT_COLOR);
+	}
+
+	private JButton buildClearModelButton() {
+		return new JButton(this.buildClearModelAction());
+	}
+
+	private Action buildClearModelAction() {
+		Action action = new AbstractAction("clear model") {
+			public void actionPerformed(ActionEvent event) {
+				RadioButtonModelAdapterUITest.this.clearModel();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void clearModel() {
+		this.testModelHolder.setValue(null);
+	}
+
+	private JButton buildRestoreModelButton() {
+		return new JButton(this.buildRestoreModelAction());
+	}
+
+	private Action buildRestoreModelAction() {
+		Action action = new AbstractAction("restore model") {
+			public void actionPerformed(ActionEvent event) {
+				RadioButtonModelAdapterUITest.this.restoreModel();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void restoreModel() {
+		this.testModelHolder.setValue(this.testModel);
+	}
+
+	private JButton buildPrintModelButton() {
+		return new JButton(this.buildPrintModelAction());
+	}
+
+	private Action buildPrintModelAction() {
+		Action action = new AbstractAction("print model") {
+			public void actionPerformed(ActionEvent event) {
+				RadioButtonModelAdapterUITest.this.printModel();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void printModel() {
+		System.out.println(this.testModel);
+	}
+
+
+	private static class TestModel extends AbstractModel {
+		private String color;
+			public static final String COLOR_PROPERTY = "color";
+			public static final String RED = "red";
+			public static final String GREEN = "green";
+			public static final String BLUE = "blue";
+			public static final String DEFAULT_COLOR = RED;
+			public static final String[] VALID_COLORS = {
+				RED,
+				GREEN,
+				BLUE
+			};
+	
+		public TestModel() {
+			this(DEFAULT_COLOR);
+		}
+		public TestModel(String color) {
+			this.color = color;
+		}
+		public String getColor() {
+			return this.color;
+		}
+		public void setColor(String color) {
+			if ( ! CollectionTools.contains(VALID_COLORS, color)) {
+				throw new IllegalArgumentException(color);
+			}
+			Object old = this.color;
+			this.color = color;
+			this.firePropertyChanged(COLOR_PROPERTY, old, color);
+		}
+		@Override
+		public String toString() {
+			return "TestModel(" + this.color + ")";
+		}
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ReadOnlyTableModelAdapterUITest.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ReadOnlyTableModelAdapterUITest.java
new file mode 100644
index 0000000..827b625
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/ReadOnlyTableModelAdapterUITest.java
@@ -0,0 +1,39 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import org.eclipse.jpt.utility.internal.model.value.swing.ColumnAdapter;
+import org.eclipse.jpt.utility.tests.internal.model.value.swing.TableModelAdapterTests.PersonColumnAdapter;
+
+/**
+ * Make it easy to test the table model adapter and
+ * renderers without any editing allowed.
+ */
+public class ReadOnlyTableModelAdapterUITest extends TableModelAdapterUITest {
+
+	public static void main(String[] args) throws Exception {
+		new ReadOnlyTableModelAdapterUITest().exec(args);
+	}
+
+	protected ReadOnlyTableModelAdapterUITest() {
+		super();
+	}
+
+	@Override
+	protected ColumnAdapter buildColumnAdapter() {
+		return new PersonColumnAdapter() {
+			@Override
+			public boolean isColumnEditable(int index) {
+				return false;
+			}
+		};
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/SpinnerModelAdapterTests.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/SpinnerModelAdapterTests.java
new file mode 100644
index 0000000..e5dce8d
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/SpinnerModelAdapterTests.java
@@ -0,0 +1,113 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import javax.swing.SpinnerModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import org.eclipse.jpt.utility.internal.ClassTools;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimplePropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+import org.eclipse.jpt.utility.internal.model.value.swing.SpinnerModelAdapter;
+import org.eclipse.jpt.utility.tests.internal.TestTools;
+
+import junit.framework.TestCase;
+
+public class SpinnerModelAdapterTests extends TestCase {
+	private PropertyValueModel valueHolder;
+	SpinnerModel spinnerModelAdapter;
+	boolean eventFired;
+
+	public SpinnerModelAdapterTests(String name) {
+		super(name);
+	}
+
+	@Override
+	protected void setUp() throws Exception {
+		super.setUp();
+		this.valueHolder = new SimplePropertyValueModel(new Integer(0));
+		this.spinnerModelAdapter = new SpinnerModelAdapter(this.valueHolder);
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		TestTools.clear(this);
+		super.tearDown();
+	}
+
+	public void testSetValueSpinnerModel() throws Exception {
+		this.eventFired = false;
+		this.spinnerModelAdapter.addChangeListener(new TestChangeListener() {
+			@Override
+			public void stateChanged(ChangeEvent e) {
+				SpinnerModelAdapterTests.this.eventFired = true;
+				assertEquals(SpinnerModelAdapterTests.this.spinnerModelAdapter, e.getSource());
+			}
+		});
+		this.spinnerModelAdapter.setValue(new Integer(5));
+		assertTrue(this.eventFired);
+		assertEquals(new Integer(5), this.valueHolder.getValue());
+	}
+
+	public void testSetValueValueHolder() throws Exception {
+		this.eventFired = false;
+		this.spinnerModelAdapter.addChangeListener(new TestChangeListener() {
+			@Override
+			public void stateChanged(ChangeEvent e) {
+				SpinnerModelAdapterTests.this.eventFired = true;
+				assertEquals(SpinnerModelAdapterTests.this.spinnerModelAdapter, e.getSource());
+			}
+		});
+		assertEquals(new Integer(0), this.spinnerModelAdapter.getValue());
+		this.valueHolder.setValue(new Integer(7));
+		assertTrue(this.eventFired);
+		assertEquals(new Integer(7), this.spinnerModelAdapter.getValue());
+	}
+
+	public void testHasListeners() throws Exception {
+		SimplePropertyValueModel localValueHolder = (SimplePropertyValueModel) this.valueHolder;
+		assertFalse(localValueHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasNoListeners(this.spinnerModelAdapter);
+
+		ChangeListener listener = new TestChangeListener();
+		this.spinnerModelAdapter.addChangeListener(listener);
+		assertTrue(localValueHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasListeners(this.spinnerModelAdapter);
+
+		this.spinnerModelAdapter.removeChangeListener(listener);
+		assertFalse(localValueHolder.hasAnyPropertyChangeListeners(ValueModel.VALUE));
+		this.verifyHasNoListeners(this.spinnerModelAdapter);
+	}
+
+	private void verifyHasNoListeners(Object adapter) throws Exception {
+		Object delegate = ClassTools.getFieldValue(adapter, "delegate");
+		Object[] listeners = (Object[]) ClassTools.executeMethod(delegate, "getChangeListeners");
+		assertEquals(0, listeners.length);
+	}
+
+	private void verifyHasListeners(Object adapter) throws Exception {
+		Object delegate = ClassTools.getFieldValue(adapter, "delegate");
+		Object[] listeners = (Object[]) ClassTools.executeMethod(delegate, "getChangeListeners");
+		assertFalse(listeners.length == 0);
+	}
+
+
+	private class TestChangeListener implements ChangeListener {
+		TestChangeListener() {
+			super();
+		}
+		public void stateChanged(ChangeEvent e) {
+			fail("unexpected event");
+		}
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/SpinnerModelAdapterUITest.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/SpinnerModelAdapterUITest.java
new file mode 100644
index 0000000..7f96e3c
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/SpinnerModelAdapterUITest.java
@@ -0,0 +1,342 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+import java.util.Calendar;
+import java.util.Date;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+import javax.swing.SpinnerModel;
+import javax.swing.WindowConstants;
+
+import org.eclipse.jpt.utility.internal.CollectionTools;
+import org.eclipse.jpt.utility.internal.model.AbstractModel;
+import org.eclipse.jpt.utility.internal.model.value.PropertyAspectAdapter;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimplePropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+import org.eclipse.jpt.utility.internal.model.value.swing.DateSpinnerModelAdapter;
+import org.eclipse.jpt.utility.internal.model.value.swing.ListSpinnerModelAdapter;
+import org.eclipse.jpt.utility.internal.model.value.swing.NumberSpinnerModelAdapter;
+
+/**
+ * Play around with a set of spinners.
+ */
+public class SpinnerModelAdapterUITest {
+
+	private TestModel testModel;
+	private PropertyValueModel testModelHolder;
+
+	private PropertyValueModel birthDateHolder;
+	private SpinnerModel birthDateSpinnerModel;
+
+	private PropertyValueModel ageHolder;
+	private SpinnerModel ageSpinnerModel;
+
+	private PropertyValueModel eyeColorHolder;
+	private SpinnerModel eyeColorSpinnerModel;
+
+
+	public static void main(String[] args) throws Exception {
+		new SpinnerModelAdapterUITest().exec(args);
+	}
+
+	private SpinnerModelAdapterUITest() {
+		super();
+	}
+
+	private void exec(String[] args) throws Exception {
+		this.testModel = new TestModel();
+		this.testModelHolder = new SimplePropertyValueModel(this.testModel);
+
+		this.birthDateHolder = this.buildBirthDateHolder(this.testModelHolder);
+		this.birthDateSpinnerModel = this.buildBirthDateSpinnerModel(this.birthDateHolder);
+
+		this.ageHolder = this.buildAgeHolder(this.testModelHolder);
+		this.ageSpinnerModel = this.buildAgeSpinnerModel(this.ageHolder);
+
+		this.eyeColorHolder = this.buildEyeColorHolder(this.testModelHolder);
+		this.eyeColorSpinnerModel = this.buildEyeColorSpinnerModel(this.eyeColorHolder);
+
+		this.openWindow();
+	}
+
+	private PropertyValueModel buildBirthDateHolder(ValueModel vm) {
+		return new PropertyAspectAdapter(vm, TestModel.BIRTH_DATE_PROPERTY) {
+			@Override
+			protected Object getValueFromSubject() {
+				return ((TestModel) this.subject).getBirthDate();
+			}
+			@Override
+			protected void setValueOnSubject(Object value) {
+				((TestModel) this.subject).setBirthDate((Date) value);
+			}
+		};
+	}
+
+	private SpinnerModel buildBirthDateSpinnerModel(PropertyValueModel valueHolder) {
+		return new DateSpinnerModelAdapter(valueHolder);
+	}
+
+	private PropertyValueModel buildAgeHolder(ValueModel vm) {
+		return new PropertyAspectAdapter(vm, TestModel.AGE_PROPERTY) {
+			@Override
+			protected Object getValueFromSubject() {
+				return new Integer(((TestModel) this.subject).getAge());
+			}
+			@Override
+			protected void setValueOnSubject(Object value) {
+				((TestModel) this.subject).setAge(((Number) value).intValue());
+			}
+		};
+	}
+
+	private SpinnerModel buildAgeSpinnerModel(PropertyValueModel valueHolder) {
+		return new NumberSpinnerModelAdapter(valueHolder, ((Integer) valueHolder.getValue()).intValue(), TestModel.MIN_AGE, TestModel.MAX_AGE, 1);
+	}
+
+	private PropertyValueModel buildEyeColorHolder(ValueModel vm) {
+		return new PropertyAspectAdapter(vm, TestModel.EYE_COLOR_PROPERTY) {
+			@Override
+			protected Object getValueFromSubject() {
+				return ((TestModel) this.subject).getEyeColor();
+			}
+			@Override
+			protected void setValueOnSubject(Object value) {
+				((TestModel) this.subject).setEyeColor((String) value);
+			}
+		};
+	}
+
+	private SpinnerModel buildEyeColorSpinnerModel(PropertyValueModel valueHolder) {
+		return new ListSpinnerModelAdapter(valueHolder, TestModel.VALID_EYE_COLORS);
+	}
+
+	private void openWindow() {
+		JFrame window = new JFrame(this.getClass().getName());
+		window.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
+		window.addWindowListener(this.buildWindowListener());
+		window.getContentPane().add(this.buildMainPanel(), "Center");
+		window.setSize(600, 100);
+		window.setVisible(true);
+	}
+
+	private WindowListener buildWindowListener() {
+		return new WindowAdapter() {
+			@Override
+			public void windowClosing(WindowEvent e) {
+				e.getWindow().setVisible(false);
+				System.exit(0);
+			}
+		};
+	}
+
+	private Component buildMainPanel() {
+		JPanel mainPanel = new JPanel(new BorderLayout());
+		mainPanel.add(this.buildSpinnerPanel(), BorderLayout.NORTH);
+		mainPanel.add(this.buildControlPanel(), BorderLayout.SOUTH);
+		return mainPanel;
+	}
+
+	private Component buildSpinnerPanel() {
+		JPanel taskListPanel = new JPanel(new GridLayout(1, 0));
+		taskListPanel.add(this.buildBirthDateSpinner());
+		taskListPanel.add(this.buildAgeSpinner());
+		taskListPanel.add(this.buildEyeColorSpinner());
+		return taskListPanel;
+	}
+
+	private JSpinner buildBirthDateSpinner() {
+		return new JSpinner(this.birthDateSpinnerModel);
+	}
+
+	private JSpinner buildAgeSpinner() {
+		return new JSpinner(this.ageSpinnerModel);
+	}
+
+	private JSpinner buildEyeColorSpinner() {
+		return new JSpinner(this.eyeColorSpinnerModel);
+	}
+
+	private Component buildControlPanel() {
+		JPanel controlPanel = new JPanel(new GridLayout(1, 0));
+		controlPanel.add(this.buildResetModelButton());
+		controlPanel.add(this.buildClearModelButton());
+		controlPanel.add(this.buildRestoreModelButton());
+		controlPanel.add(this.buildPrintModelButton());
+		return controlPanel;
+	}
+
+	private JButton buildResetModelButton() {
+		return new JButton(this.buildResetModelAction());
+	}
+
+	private Action buildResetModelAction() {
+		Action action = new AbstractAction("reset model") {
+			public void actionPerformed(ActionEvent event) {
+				SpinnerModelAdapterUITest.this.resetModel();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void resetModel() {
+		this.testModel.setBirthDate(TestModel.DEFAULT_BIRTH_DATE);
+		this.testModel.setEyeColor(TestModel.DEFAULT_EYE_COLOR);
+	}
+
+	private JButton buildClearModelButton() {
+		return new JButton(this.buildClearModelAction());
+	}
+
+	private Action buildClearModelAction() {
+		Action action = new AbstractAction("clear model") {
+			public void actionPerformed(ActionEvent event) {
+				SpinnerModelAdapterUITest.this.clearModel();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void clearModel() {
+		this.testModelHolder.setValue(null);
+	}
+
+	private JButton buildRestoreModelButton() {
+		return new JButton(this.buildRestoreModelAction());
+	}
+
+	private Action buildRestoreModelAction() {
+		Action action = new AbstractAction("restore model") {
+			public void actionPerformed(ActionEvent event) {
+				SpinnerModelAdapterUITest.this.restoreModel();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void restoreModel() {
+		this.testModelHolder.setValue(this.testModel);
+	}
+
+	private JButton buildPrintModelButton() {
+		return new JButton(this.buildPrintModelAction());
+	}
+
+	private Action buildPrintModelAction() {
+		Action action = new AbstractAction("print model") {
+			public void actionPerformed(ActionEvent event) {
+				SpinnerModelAdapterUITest.this.printModel();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void printModel() {
+		System.out.println("birth date: " + this.testModel.getBirthDate());
+		System.out.println("age: " + this.testModel.getAge());
+		System.out.println("eyes: " + this.testModel.getEyeColor());
+	}
+
+
+	private static class TestModel extends AbstractModel {
+		private Calendar birthCal = Calendar.getInstance();
+			// "virtual" properties
+			public static final String BIRTH_DATE_PROPERTY = "birthDate";
+			public static final String AGE_PROPERTY = "age";
+			public static final Date DEFAULT_BIRTH_DATE = new Date();
+			public static final int DEFAULT_AGE = 0;
+			public static final int MIN_AGE = 0;
+			public static final int MAX_AGE = 150;
+		private String eyeColor;
+			public static final String EYE_COLOR_PROPERTY = "eyeColor";
+			public static final String[] VALID_EYE_COLORS = {"blue", "brown", "green", "hazel", "pink"};
+			public static final String DEFAULT_EYE_COLOR = VALID_EYE_COLORS[3];
+	
+		public TestModel() {
+			this(DEFAULT_BIRTH_DATE, DEFAULT_EYE_COLOR);
+		}
+		public TestModel(Date birthDate, String eyeColor) {
+			this.setBirthDate(birthDate);
+			this.setEyeColor(eyeColor);
+		}
+		public Date getBirthDate() {
+			return (Date) this.birthCal.getTime().clone();
+		}
+		public void setBirthDate(Date birthDate) {
+			Date oldBirthDate = this.getBirthDate();
+			int oldAge = this.getAge();
+			this.birthCal.setTimeInMillis(birthDate.getTime());
+			int newAge = this.getAge();
+			if (newAge < MIN_AGE || newAge > MAX_AGE) {
+				throw new IllegalArgumentException(birthDate.toString());
+			}
+			this.firePropertyChanged(BIRTH_DATE_PROPERTY, oldBirthDate, this.getBirthDate());
+			this.firePropertyChanged(AGE_PROPERTY, oldAge, newAge);
+		}
+		public int getAge() {
+			Calendar currentCal = Calendar.getInstance();
+			int age = currentCal.get(Calendar.YEAR) - this.birthCal.get(Calendar.YEAR);
+			if (currentCal.get(Calendar.MONTH) < this.birthCal.get(Calendar.MONTH)) {
+				age--;
+			} else if (currentCal.get(Calendar.MONTH) == this.birthCal.get(Calendar.MONTH)) {
+				if (currentCal.get(Calendar.DAY_OF_MONTH) < this.birthCal.get(Calendar.DAY_OF_MONTH)) {
+					age--;
+				}
+			}
+			return age;
+		}
+		public void setAge(int newAge) {
+			if (newAge < MIN_AGE || newAge > MAX_AGE) {
+				throw new IllegalArgumentException(String.valueOf(newAge));
+			}
+	
+			int oldAge = this.getAge();
+			int delta = newAge - oldAge;
+	
+			Calendar newBirthCal = Calendar.getInstance();
+			newBirthCal.setTimeInMillis(this.birthCal.getTime().getTime());
+			// if the age increased, the birth date must be "decreased"; and vice versa
+			newBirthCal.set(Calendar.YEAR, newBirthCal.get(Calendar.YEAR) - delta);
+			this.setBirthDate(newBirthCal.getTime());
+		}
+		public String getEyeColor() {
+			return this.eyeColor;
+		}
+		public void setEyeColor(String eyeColor) {
+			if ( ! CollectionTools.contains(VALID_EYE_COLORS, eyeColor)) {
+				throw new IllegalArgumentException(eyeColor);
+			}
+			Object old = this.eyeColor;
+			this.eyeColor = eyeColor;
+			this.firePropertyChanged(EYE_COLOR_PROPERTY, old, eyeColor);
+		}
+		public String toString() {
+			return "TestModel(birth: " + this.getBirthDate() + " - eyes: " + this.eyeColor + ")";
+		}
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/TableModelAdapterTests.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/TableModelAdapterTests.java
new file mode 100644
index 0000000..4d6783e
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/TableModelAdapterTests.java
@@ -0,0 +1,634 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.swing.event.TableModelEvent;
+import javax.swing.event.TableModelListener;
+
+import org.eclipse.jpt.utility.internal.CollectionTools;
+import org.eclipse.jpt.utility.internal.StringTools;
+import org.eclipse.jpt.utility.internal.iterators.CloneIterator;
+import org.eclipse.jpt.utility.internal.iterators.TransformationIterator;
+import org.eclipse.jpt.utility.internal.model.AbstractModel;
+import org.eclipse.jpt.utility.internal.model.value.CollectionAspectAdapter;
+import org.eclipse.jpt.utility.internal.model.value.CollectionValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ListValueModel;
+import org.eclipse.jpt.utility.internal.model.value.PropertyAspectAdapter;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SortedListValueModelAdapter;
+import org.eclipse.jpt.utility.internal.model.value.swing.ColumnAdapter;
+import org.eclipse.jpt.utility.internal.model.value.swing.TableModelAdapter;
+import org.eclipse.jpt.utility.tests.internal.TestTools;
+
+import junit.framework.TestCase;
+
+/**
+ * 
+ */
+public class TableModelAdapterTests extends TestCase {
+	private Crowd crowd;
+	TableModelEvent event;
+
+	public TableModelAdapterTests(String name) {
+		super(name);
+	}
+
+	@Override
+	protected void setUp() throws Exception {
+		super.setUp();
+		this.crowd = this.buildCrowd();
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		TestTools.clear(this);
+		super.tearDown();
+	}
+
+	public void testGetRowCount() throws Exception {
+		TableModelAdapter tableModelAdapter =  this.buildTableModelAdapter();
+		assertEquals(0, tableModelAdapter.getRowCount());
+		// we need to add a listener to wake up the adapter
+		tableModelAdapter.addTableModelListener(this.buildTableModelListener());
+		assertEquals(this.crowd.peopleSize(), tableModelAdapter.getRowCount());
+	}
+
+	public void testGetColumnCount() throws Exception {
+		TableModelAdapter tableModelAdapter =  this.buildTableModelAdapter();
+		assertEquals(PersonColumnAdapter.COLUMN_COUNT, tableModelAdapter.getColumnCount());
+	}
+
+	public void testGetValueAt() throws Exception {
+		TableModelAdapter tableModelAdapter =  this.buildTableModelAdapter();
+		tableModelAdapter.addTableModelListener(this.buildTableModelListener());
+
+		List<String> sortedNames = this.sortedNames();
+		for (int i = 0; i < this.crowd.peopleSize(); i++) {
+			assertEquals(sortedNames.get(i), tableModelAdapter.getValueAt(i, PersonColumnAdapter.NAME_COLUMN));
+		}
+	}
+
+	public void testSetValueAt() throws Exception {
+		TableModelAdapter tableModelAdapter =  this.buildTableModelAdapter();
+		this.event = null;
+		tableModelAdapter.addTableModelListener(new TestTableModelListener() {
+			@Override
+			public void tableChanged(TableModelEvent e) {
+				TableModelAdapterTests.this.event = e;
+			}
+		});
+
+		Person person = this.crowd.personNamed("Gollum");
+		assertEquals(Person.EYE_COLOR_BLUE, person.getEyeColor());
+		assertFalse(person.isEvil());
+		assertEquals(0, person.getRank());
+
+		for (int i = 0; i < tableModelAdapter.getRowCount(); i++) {
+			if (tableModelAdapter.getValueAt(i, PersonColumnAdapter.NAME_COLUMN).equals("Gollum")) {
+				tableModelAdapter.setValueAt(Person.EYE_COLOR_HAZEL, i, PersonColumnAdapter.EYE_COLOR_COLUMN);
+				tableModelAdapter.setValueAt(Boolean.TRUE, i, PersonColumnAdapter.EVIL_COLUMN);
+				tableModelAdapter.setValueAt(new Integer(-1), i, PersonColumnAdapter.RANK_COLUMN);
+				break;
+			}
+		}
+		assertNotNull(this.event);
+		assertEquals(Person.EYE_COLOR_HAZEL, person.getEyeColor());
+		assertTrue(person.isEvil());
+		assertEquals(-1, person.getRank());
+	}
+
+	public void testAddRow() throws Exception {
+		TableModelAdapter tableModelAdapter =  this.buildTableModelAdapter();
+		this.event = null;
+		tableModelAdapter.addTableModelListener(this.buildSingleEventListener());
+		// add a person to the end of the list so we only trigger one event
+		this.crowd.addPerson("Zzzzz");
+		assertNotNull(this.event);
+		assertEquals(TableModelEvent.INSERT, this.event.getType());
+		assertEquals(TableModelEvent.ALL_COLUMNS, this.event.getColumn());
+	}
+
+	public void testRemoveRow() throws Exception {
+		TableModelAdapter tableModelAdapter =  this.buildTableModelAdapter();
+		this.event = null;
+		tableModelAdapter.addTableModelListener(this.buildSingleEventListener());
+		// removing a person should only trigger one event, since a re-sort is not needed
+		this.crowd.removePerson(this.crowd.personNamed("Gollum"));
+		assertNotNull(this.event);
+		assertEquals(TableModelEvent.DELETE, this.event.getType());
+		assertEquals(TableModelEvent.ALL_COLUMNS, this.event.getColumn());
+	}
+
+	public void testChangeCell() throws Exception {
+		TableModelAdapter tableModelAdapter =  this.buildTableModelAdapter();
+		this.event = null;
+		tableModelAdapter.addTableModelListener(this.buildSingleEventListener());
+		// add a person to the end of the list so we only trigger one event
+		Person person = this.crowd.personNamed("Gollum");
+		person.setEvil(true);
+		assertNotNull(this.event);
+		assertEquals(TableModelEvent.UPDATE, this.event.getType());
+		assertEquals(PersonColumnAdapter.EVIL_COLUMN, this.event.getColumn());
+	}
+
+	public void testLazyListListener() throws Exception {
+		TableModelAdapter tableModelAdapter =  this.buildTableModelAdapter();
+		TableModelListener listener = this.buildTableModelListener();
+		assertTrue(this.crowd.hasNoCollectionChangeListeners(Crowd.PEOPLE_COLLECTION));
+		tableModelAdapter.addTableModelListener(listener);
+		assertTrue(this.crowd.hasAnyCollectionChangeListeners(Crowd.PEOPLE_COLLECTION));
+		tableModelAdapter.removeTableModelListener(listener);
+		assertTrue(this.crowd.hasNoCollectionChangeListeners(Crowd.PEOPLE_COLLECTION));
+	}
+
+	public void testLazyCellListener() throws Exception {
+		TableModelAdapter tableModelAdapter =  this.buildTableModelAdapter();
+		TableModelListener listener = this.buildTableModelListener();
+		Person person = this.crowd.personNamed("Gollum");
+		assertTrue(person.hasNoPropertyChangeListeners(Person.NAME_PROPERTY));
+		assertTrue(person.hasNoPropertyChangeListeners(Person.BIRTH_DATE_PROPERTY));
+		assertTrue(person.hasNoPropertyChangeListeners(Person.EYE_COLOR_PROPERTY));
+		assertTrue(person.hasNoPropertyChangeListeners(Person.EVIL_PROPERTY));
+		assertTrue(person.hasNoPropertyChangeListeners(Person.RANK_PROPERTY));
+
+		tableModelAdapter.addTableModelListener(listener);
+		assertTrue(person.hasAnyPropertyChangeListeners(Person.NAME_PROPERTY));
+		assertTrue(person.hasAnyPropertyChangeListeners(Person.BIRTH_DATE_PROPERTY));
+		assertTrue(person.hasAnyPropertyChangeListeners(Person.EYE_COLOR_PROPERTY));
+		assertTrue(person.hasAnyPropertyChangeListeners(Person.EVIL_PROPERTY));
+		assertTrue(person.hasAnyPropertyChangeListeners(Person.RANK_PROPERTY));
+
+		tableModelAdapter.removeTableModelListener(listener);
+		assertTrue(person.hasNoPropertyChangeListeners(Person.NAME_PROPERTY));
+		assertTrue(person.hasNoPropertyChangeListeners(Person.BIRTH_DATE_PROPERTY));
+		assertTrue(person.hasNoPropertyChangeListeners(Person.EYE_COLOR_PROPERTY));
+		assertTrue(person.hasNoPropertyChangeListeners(Person.EVIL_PROPERTY));
+		assertTrue(person.hasNoPropertyChangeListeners(Person.RANK_PROPERTY));
+	}
+
+	private TableModelAdapter buildTableModelAdapter() {
+		return new TableModelAdapter(this.buildSortedPeopleAdapter(), this.buildColumnAdapter());
+	}
+
+	private ListValueModel buildSortedPeopleAdapter() {
+		return new SortedListValueModelAdapter(this.buildPeopleAdapter());
+	}
+
+	private CollectionValueModel buildPeopleAdapter() {
+		return new CollectionAspectAdapter(Crowd.PEOPLE_COLLECTION, this.crowd) {
+			@Override
+			protected Iterator<Person> getValueFromSubject() {
+				return ((Crowd) this.subject).people();
+			}
+			@Override
+			protected int sizeFromSubject() {
+				return ((Crowd) this.subject).peopleSize();
+			}
+		};
+	}
+
+	private Crowd buildCrowd() {
+		Crowd result = new Crowd();
+		result.addPerson("Bilbo");
+		result.addPerson("Gollum");
+		result.addPerson("Frodo");
+		result.addPerson("Samwise");
+		return result;
+	}
+
+	private ColumnAdapter buildColumnAdapter() {
+		return new PersonColumnAdapter();
+	}
+
+	private TableModelListener buildTableModelListener() {
+		return new TestTableModelListener();
+	}
+
+	private List<String> sortedNames() {
+		return new ArrayList<String>(CollectionTools.sortedSet(this.crowd.peopleNames()));
+	}
+
+	private TableModelListener buildSingleEventListener() {
+		return new TestTableModelListener() {
+			@Override
+			public void tableChanged(TableModelEvent e) {
+				// we expect only a single event
+				if (TableModelAdapterTests.this.event == null) {
+					TableModelAdapterTests.this.event = e;
+				} else {
+					fail("unexpected event");
+				}
+			}
+		};
+	}
+
+
+	// ********** classes **********
+
+	public static class PersonColumnAdapter implements ColumnAdapter {
+		public static final int COLUMN_COUNT = 7;
+	
+		public static final int NAME_COLUMN = 0;
+		public static final int BIRTH_DATE_COLUMN = 1;
+		public static final int GONE_WEST_DATE_COLUMN = 2;
+		public static final int EYE_COLOR_COLUMN = 3;
+		public static final int EVIL_COLUMN = 4;
+		public static final int RANK_COLUMN = 5;
+		public static final int ADVENTURE_COUNT_COLUMN = 6;
+	
+		private static final String[] COLUMN_NAMES = new String[] {
+			"Name",
+			"Birth",
+			"Gone West",
+			"Eyes",
+			"Evil",
+			"Rank",
+			"Adventures"
+		};
+	
+	
+		public int getColumnCount() {
+			return COLUMN_COUNT;
+		}
+	
+		public String getColumnName(int index) {
+			return COLUMN_NAMES[index];
+		}
+	
+		public Class<?> getColumnClass(int index) {
+			switch (index) {
+				case NAME_COLUMN:					return Object.class;
+				case BIRTH_DATE_COLUMN:			return Date.class;
+				case GONE_WEST_DATE_COLUMN:	return Date.class;
+				case EYE_COLOR_COLUMN:			return Object.class;
+				case EVIL_COLUMN:					return Boolean.class;
+				case RANK_COLUMN:					return Integer.class;
+				case ADVENTURE_COUNT_COLUMN:return Integer.class;
+				default: 									return Object.class;
+			}
+		}
+	
+		public boolean isColumnEditable(int index) {
+			return index != NAME_COLUMN;
+		}
+	
+		public PropertyValueModel[] cellModels(Object subject) {
+			Person person = (Person) subject;
+			PropertyValueModel[] result = new PropertyValueModel[COLUMN_COUNT];
+	
+			result[NAME_COLUMN]						= this.buildNameAdapter(person);
+			result[BIRTH_DATE_COLUMN]				= this.buildBirthDateAdapter(person);
+			result[GONE_WEST_DATE_COLUMN]	= this.buildGoneWestDateAdapter(person);
+			result[EYE_COLOR_COLUMN]				= this.buildEyeColorAdapter(person);
+			result[EVIL_COLUMN]						= this.buildEvilAdapter(person);
+			result[RANK_COLUMN]						= this.buildRankAdapter(person);
+			result[ADVENTURE_COUNT_COLUMN]	= this.buildAdventureCountAdapter(person);
+	
+			return result;
+		}
+	
+		private PropertyValueModel buildNameAdapter(Person person) {
+			return new PropertyAspectAdapter(Person.NAME_PROPERTY, person) {
+				@Override
+				protected Object getValueFromSubject() {
+					return ((Person) this.subject).getName();
+				}
+				@Override
+				protected void setValueOnSubject(Object value) {
+					((Person) this.subject).setName((String) value);
+				}
+			};
+		}
+	
+		private PropertyValueModel buildBirthDateAdapter(Person person) {
+			return new PropertyAspectAdapter(Person.BIRTH_DATE_PROPERTY, person) {
+				@Override
+				protected Object getValueFromSubject() {
+					return ((Person) this.subject).getBirthDate();
+				}
+				@Override
+				protected void setValueOnSubject(Object value) {
+					((Person) this.subject).setBirthDate((Date) value);
+				}
+			};
+		}
+	
+		private PropertyValueModel buildGoneWestDateAdapter(Person person) {
+			return new PropertyAspectAdapter(Person.GONE_WEST_DATE_PROPERTY, person) {
+				@Override
+				protected Object getValueFromSubject() {
+					return ((Person) this.subject).getGoneWestDate();
+				}
+				@Override
+				protected void setValueOnSubject(Object value) {
+					((Person) this.subject).setGoneWestDate((Date) value);
+				}
+			};
+		}
+	
+		private PropertyValueModel buildEyeColorAdapter(Person person) {
+			return new PropertyAspectAdapter(Person.EYE_COLOR_PROPERTY, person) {
+				@Override
+				protected Object getValueFromSubject() {
+					return ((Person) this.subject).getEyeColor();
+				}
+				@Override
+				protected void setValueOnSubject(Object value) {
+					((Person) this.subject).setEyeColor((String) value);
+				}
+			};
+		}
+	
+		private PropertyValueModel buildEvilAdapter(Person person) {
+			return new PropertyAspectAdapter(Person.EVIL_PROPERTY, person) {
+				@Override
+				protected Object getValueFromSubject() {
+					return Boolean.valueOf(((Person) this.subject).isEvil());
+				}
+				@Override
+				protected void setValueOnSubject(Object value) {
+					((Person) this.subject).setEvil(((Boolean) value).booleanValue());
+				}
+			};
+		}
+	
+		private PropertyValueModel buildRankAdapter(Person person) {
+			return new PropertyAspectAdapter(Person.RANK_PROPERTY, person) {
+				@Override
+				protected Object getValueFromSubject() {
+					return new Integer(((Person) this.subject).getRank());
+				}
+				@Override
+				protected void setValueOnSubject(Object value) {
+					((Person) this.subject).setRank(((Integer) value).intValue());
+				}
+			};
+		}
+	
+		private PropertyValueModel buildAdventureCountAdapter(Person person) {
+			return new PropertyAspectAdapter(Person.ADVENTURE_COUNT_PROPERTY, person) {
+				@Override
+				protected Object getValueFromSubject() {
+					return new Integer(((Person) this.subject).getAdventureCount());
+				}
+				@Override
+				protected void setValueOnSubject(Object value) {
+					((Person) this.subject).setAdventureCount(((Integer) value).intValue());
+				}
+			};
+		}
+	
+	}
+
+
+	public static class Crowd extends AbstractModel {
+		private final Collection<Person> people;
+			public static final String PEOPLE_COLLECTION = "people";
+	
+		public Crowd() {
+			super();
+			this.people = new ArrayList<Person>();
+		}
+	
+	
+		public Iterator<Person> people() {
+			return new CloneIterator<Person>(this.people) {
+				@Override
+				protected void remove(Person person) {
+					Crowd.this.removePerson(person);
+				}
+			};
+		}
+	
+		public int peopleSize() {
+			return this.people.size();
+		}
+	
+		public Person addPerson(String name) {
+			this.checkPersonName(name);
+			return this.addPerson(new Person(this, name));
+		}
+	
+		private Person addPerson(Person person) {
+			this.addItemToCollection(person, this.people, PEOPLE_COLLECTION);
+			return person;
+		}
+	
+		public void removePerson(Person person) {
+			this.removeItemFromCollection(person, this.people, PEOPLE_COLLECTION);
+		}
+	
+		public void removePeople(Collection<Person> persons) {
+			this.removeItemsFromCollection(persons, this.people, PEOPLE_COLLECTION);
+		}
+	
+		public void removePeople(Iterator<Person> persons) {
+			this.removeItemsFromCollection(persons, this.people, PEOPLE_COLLECTION);
+		}
+	
+		void checkPersonName(String personName) {
+			if (personName == null) {
+				throw new NullPointerException();
+			}
+			if (CollectionTools.contains(this.peopleNames(), personName)) {
+				throw new IllegalArgumentException(personName);
+			}
+		}
+	
+		public Iterator<String> peopleNames() {
+			return new TransformationIterator<Person, String>(this.people.iterator()) {
+				@Override
+				protected String transform(Person person) {
+					return person.getName();
+				}
+			};
+		}
+	
+		public Person personNamed(String name) {
+			for (Iterator<Person> stream = this.people.iterator(); stream.hasNext(); ) {
+				Person person = stream.next();
+				if (person.getName().equals(name)) {
+					return person;
+				}
+			}
+			return null;
+		}
+	
+		@Override
+		public String toString() {
+			return StringTools.buildToStringFor(this, String.valueOf(this.people.size()) + " people");
+		}
+	
+	}
+	
+	
+	public static class Person extends AbstractModel implements Comparable<Person> {
+		private Crowd crowd;
+		private String name;
+			public static final String NAME_PROPERTY= "name";
+		private Date birthDate;
+			public static final String BIRTH_DATE_PROPERTY= "birthDate";
+		private Date goneWestDate;
+			public static final String GONE_WEST_DATE_PROPERTY= "goneWestDate";
+		private String eyeColor;
+			public static final String EYE_COLOR_PROPERTY= "eyeColor";
+			public static final String EYE_COLOR_BLUE = "blue";
+			public static final String EYE_COLOR_GREEN = "green";
+			public static final String EYE_COLOR_BROWN = "brown";
+			public static final String EYE_COLOR_HAZEL = "hazel";
+			public static final String EYE_COLOR_PINK = "pink";
+			private static Collection<String> validEyeColors;
+			public static final String DEFAULT_EYE_COLOR = EYE_COLOR_BLUE;
+		private boolean evil;
+			public static final String EVIL_PROPERTY= "evil";
+		private int rank;
+			public static final String RANK_PROPERTY= "rank";
+		private int adventureCount;
+			public static final String ADVENTURE_COUNT_PROPERTY= "adventureCount";
+	
+		Person(Crowd crowd, String name) {
+			super();
+			this.crowd = crowd;
+			this.name = name;
+			this.birthDate = new Date();
+			Calendar c = Calendar.getInstance();
+			c.add(Calendar.YEAR, 250);
+			this.goneWestDate = new Date(c.getTimeInMillis());
+			this.eyeColor = DEFAULT_EYE_COLOR;
+			this.evil = false;
+			this.rank = 0;
+			this.adventureCount = 0;
+		}
+	
+		public static Collection<String> getValidEyeColors() {
+			if (validEyeColors == null) {
+				validEyeColors = buildValidEyeColors();
+			}
+			return validEyeColors;
+		}
+	
+		private static Collection<String> buildValidEyeColors() {
+			Collection<String> result = new ArrayList<String>();
+			result.add(EYE_COLOR_BLUE);
+			result.add(EYE_COLOR_GREEN);
+			result.add(EYE_COLOR_BROWN);
+			result.add(EYE_COLOR_HAZEL);
+			result.add(EYE_COLOR_PINK);
+			return result;
+		}
+	
+		public Crowd getCrowd() {
+			return this.crowd;
+		}
+	
+		public String getName() {
+			return this.name;
+		}
+		public void setName(String name) {
+			this.crowd.checkPersonName(name);
+			Object old = this.name;
+			this.name = name;
+			this.firePropertyChanged(NAME_PROPERTY, old, name);
+		}
+	
+		public Date getBirthDate() {
+			return this.birthDate;
+		}
+		public void setBirthDate(Date birthDate) {
+			Object old = this.birthDate;
+			this.birthDate = birthDate;
+			this.firePropertyChanged(BIRTH_DATE_PROPERTY, old, birthDate);
+		}
+	
+		public Date getGoneWestDate() {
+			return this.goneWestDate;
+		}
+		public void setGoneWestDate(Date goneWestDate) {
+			Object old = this.goneWestDate;
+			this.goneWestDate = goneWestDate;
+			this.firePropertyChanged(GONE_WEST_DATE_PROPERTY, old, goneWestDate);
+		}
+	
+		public String getEyeColor() {
+			return this.eyeColor;
+		}
+		public void setEyeColor(String eyeColor) {
+			if (! getValidEyeColors().contains(eyeColor)) {
+				throw new IllegalArgumentException(eyeColor);
+			}
+			Object old = this.eyeColor;
+			this.eyeColor = eyeColor;
+			this.firePropertyChanged(EYE_COLOR_PROPERTY, old, eyeColor);
+		}
+	
+		public boolean isEvil() {
+			return this.evil;
+		}
+		public void setEvil(boolean evil) {
+			boolean old = this.evil;
+			this.evil = evil;
+			this.firePropertyChanged(EVIL_PROPERTY, old, evil);
+		}
+	
+		public int getRank() {
+			return this.rank;
+		}
+		public void setRank(int rank) {
+			int old = this.rank;
+			this.rank = rank;
+			this.firePropertyChanged(RANK_PROPERTY, old, rank);
+		}
+	
+		public int getAdventureCount() {
+			return this.adventureCount;
+		}
+		public void setAdventureCount(int adventureCount) {
+			int old = this.adventureCount;
+			this.adventureCount = adventureCount;
+			this.firePropertyChanged(ADVENTURE_COUNT_PROPERTY, old, adventureCount);
+		}
+	
+		public int compareTo(Person p) {
+			return this.name.compareToIgnoreCase(p.name);
+		}
+	
+		@Override
+		public String toString() {
+			return this.name + 
+						"\tborn: " + DateFormat.getDateInstance().format(this.birthDate) + 
+						"\tgone west: " + DateFormat.getDateInstance().format(this.goneWestDate) + 
+						"\teyes: " + this.eyeColor + 
+						"\tevil: " + this.evil + 
+						"\trank: " + this.rank +
+						"\tadventures: " + this.adventureCount
+			;
+		}
+	
+	}
+
+
+	private class TestTableModelListener implements TableModelListener {
+		TestTableModelListener() {
+			super();
+		}
+		public void tableChanged(TableModelEvent e) {
+			fail("unexpected event");
+		}
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/TableModelAdapterUITest.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/TableModelAdapterUITest.java
new file mode 100644
index 0000000..3e02eaa
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/TableModelAdapterUITest.java
@@ -0,0 +1,732 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.GridLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+import java.util.Date;
+import java.util.Iterator;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.ButtonModel;
+import javax.swing.ComboBoxModel;
+import javax.swing.DefaultListCellRenderer;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JFrame;
+import javax.swing.JList;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSpinner;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.ListCellRenderer;
+import javax.swing.ListSelectionModel;
+import javax.swing.SpinnerModel;
+import javax.swing.UIManager;
+import javax.swing.WindowConstants;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.TableColumn;
+import javax.swing.table.TableModel;
+import javax.swing.text.Document;
+
+import org.eclipse.jpt.utility.internal.ClassTools;
+import org.eclipse.jpt.utility.internal.CollectionTools;
+import org.eclipse.jpt.utility.internal.model.value.CollectionAspectAdapter;
+import org.eclipse.jpt.utility.internal.model.value.CollectionValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ItemPropertyListValueModelAdapter;
+import org.eclipse.jpt.utility.internal.model.value.ListValueModel;
+import org.eclipse.jpt.utility.internal.model.value.PropertyAspectAdapter;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimpleCollectionValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimplePropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SortedListValueModelAdapter;
+import org.eclipse.jpt.utility.internal.model.value.swing.CheckBoxModelAdapter;
+import org.eclipse.jpt.utility.internal.model.value.swing.ColumnAdapter;
+import org.eclipse.jpt.utility.internal.model.value.swing.ComboBoxModelAdapter;
+import org.eclipse.jpt.utility.internal.model.value.swing.DateSpinnerModelAdapter;
+import org.eclipse.jpt.utility.internal.model.value.swing.DocumentAdapter;
+import org.eclipse.jpt.utility.internal.model.value.swing.ListModelAdapter;
+import org.eclipse.jpt.utility.internal.model.value.swing.NumberSpinnerModelAdapter;
+import org.eclipse.jpt.utility.internal.model.value.swing.ObjectListSelectionModel;
+import org.eclipse.jpt.utility.internal.model.value.swing.TableModelAdapter;
+import org.eclipse.jpt.utility.internal.swing.CheckBoxTableCellRenderer;
+import org.eclipse.jpt.utility.internal.swing.ComboBoxTableCellRenderer;
+import org.eclipse.jpt.utility.internal.swing.SpinnerTableCellRenderer;
+import org.eclipse.jpt.utility.internal.swing.TableCellEditorAdapter;
+import org.eclipse.jpt.utility.tests.internal.model.value.swing.TableModelAdapterTests.Crowd;
+import org.eclipse.jpt.utility.tests.internal.model.value.swing.TableModelAdapterTests.Person;
+import org.eclipse.jpt.utility.tests.internal.model.value.swing.TableModelAdapterTests.PersonColumnAdapter;
+
+/**
+ * an example UI for testing the TableModelAdapter
+ * 	"name" column is read-only text field
+ * 	"birth date" column is date text field
+ * 	"gone west date" column is date spinner
+ * 	"eye color" column is combo-box
+ * 	"evil" column is check box
+ * 	"rank" column is number text field
+ * 	"adventure count" column is number spinner
+ * 
+ * Note that the table model and row selection model share the same
+ * list value model (the sorted people adapter)
+ */
+public class TableModelAdapterUITest {
+	private CollectionValueModel eyeColorListHolder;
+	private PropertyValueModel crowdHolder;
+	private PropertyValueModel selectedPersonHolder;
+	private ListValueModel sortedPeopleAdapter;
+	private TableModel tableModel;
+	private ObjectListSelectionModel rowSelectionModel;
+	private Action removeAction;
+	private Action renameAction;
+
+	public static void main(String[] args) throws Exception {
+		new TableModelAdapterUITest().exec(args);
+	}
+
+	protected TableModelAdapterUITest() {
+		super();
+	}
+
+	protected void exec(String[] args) throws Exception {
+		UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+		this.eyeColorListHolder = this. buildEyeColorCollectionHolder();
+		this.crowdHolder = this.buildCrowdHolder();
+		this.selectedPersonHolder = this.buildSelectedPersonHolder();
+		this.sortedPeopleAdapter = this.buildSortedPeopleAdapter();
+		this.tableModel = this.buildTableModel();
+		this.rowSelectionModel = this.buildRowSelectionModel();
+		this.openWindow();
+	}
+
+	private CollectionValueModel buildEyeColorCollectionHolder() {
+		return new SimpleCollectionValueModel(Person.getValidEyeColors());
+	}
+
+	private PropertyValueModel buildCrowdHolder() {
+		return new SimplePropertyValueModel(this.buildCrowd());
+	}
+
+	private Crowd buildCrowd() {
+		Crowd crowd = new Crowd();
+
+		Person p = crowd.addPerson("Bilbo");
+		p.setEyeColor(Person.EYE_COLOR_BROWN);
+		p.setRank(22);
+		p.setAdventureCount(1);
+
+		p = crowd.addPerson("Gollum");
+		p.setEyeColor(Person.EYE_COLOR_PINK);
+		p.setEvil(true);
+		p.setRank(2);
+		p.setAdventureCount(50);
+
+		p = crowd.addPerson("Frodo");
+		p.setEyeColor(Person.EYE_COLOR_BLUE);
+		p.setRank(34);
+		p.setAdventureCount(1);
+
+		p = crowd.addPerson("Samwise");
+		p.setEyeColor(Person.EYE_COLOR_GREEN);
+		p.setRank(19);
+		p.setAdventureCount(1);
+
+		return crowd;
+	}
+
+	private PropertyValueModel buildSelectedPersonHolder() {
+		return new SimplePropertyValueModel();
+	}
+
+	private ListValueModel buildSortedPeopleAdapter() {
+		return new SortedListValueModelAdapter(this.buildPeopleNameAdapter());
+	}
+
+	// the list will need to be re-sorted if a name changes
+	private ListValueModel buildPeopleNameAdapter() {
+		return new ItemPropertyListValueModelAdapter(this.buildPeopleAdapter(), Person.NAME_PROPERTY);
+	}
+
+	private CollectionValueModel buildPeopleAdapter() {
+		return new CollectionAspectAdapter(this.crowdHolder, Crowd.PEOPLE_COLLECTION) {
+			@Override
+			protected Iterator getValueFromSubject() {
+				return ((Crowd) this.subject).people();
+			}
+			@Override
+			protected int sizeFromSubject() {
+				return ((Crowd) this.subject).peopleSize();
+			}
+		};
+	}
+
+	private TableModel buildTableModel() {
+		return new TableModelAdapter(this.sortedPeopleAdapter, this.buildColumnAdapter());
+	}
+
+	protected ColumnAdapter buildColumnAdapter() {
+		return new PersonColumnAdapter();
+	}
+
+	private ObjectListSelectionModel buildRowSelectionModel() {
+		ObjectListSelectionModel rsm = new ObjectListSelectionModel(new ListModelAdapter(this.sortedPeopleAdapter));
+		rsm.addListSelectionListener(this.buildRowSelectionListener());
+		rsm.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+		return rsm;
+	}
+
+	private ListSelectionListener buildRowSelectionListener() {
+		return new ListSelectionListener() {
+			public void valueChanged(ListSelectionEvent e) {
+				if (e.getValueIsAdjusting()) {
+					return;
+				}
+				TableModelAdapterUITest.this.rowSelectionChanged(e);
+			}
+		};
+	}
+
+	void rowSelectionChanged(ListSelectionEvent e) {
+		Object selection = this.rowSelectionModel.getSelectedValue();
+		this.selectedPersonHolder.setValue(selection);
+		boolean personSelected = (selection != null);
+		this.removeAction.setEnabled(personSelected);
+		this.renameAction.setEnabled(personSelected);
+	}
+
+	private void openWindow() {
+		JFrame window = new JFrame(ClassTools.shortClassNameForObject(this));
+		window.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
+		window.addWindowListener(this.buildWindowListener());
+		window.getContentPane().add(this.buildMainPanel(), "Center");
+		window.setLocation(200, 200);
+		window.setSize(600, 400);
+		window.setVisible(true);
+	}
+
+	private WindowListener buildWindowListener() {
+		return new WindowAdapter() {
+			@Override
+			public void windowClosing(WindowEvent e) {
+				e.getWindow().setVisible(false);
+				System.exit(0);
+			}
+		};
+	}
+
+	private Component buildMainPanel() {
+		JPanel mainPanel = new JPanel(new BorderLayout());
+		mainPanel.add(this.buildTablePane(), BorderLayout.CENTER);
+		mainPanel.add(this.buildControlPanel(), BorderLayout.SOUTH);
+		return mainPanel;
+	}
+
+	private Component buildTablePane() {
+		return new JScrollPane(this.buildTable());
+	}
+
+	private JTable buildTable() {
+		JTable table = new JTable(this.tableModel);
+		table.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);	// see Java bug 5007652
+		table.setSelectionModel(this.rowSelectionModel);
+		table.setDoubleBuffered(true);
+		table.setAutoResizeMode(JTable.AUTO_RESIZE_NEXT_COLUMN);
+		int rowHeight = 20;	// start with minimum of 20
+
+		// gone west column (spinner)
+		TableColumn column = table.getColumnModel().getColumn(PersonColumnAdapter.GONE_WEST_DATE_COLUMN);
+		SpinnerTableCellRenderer spinnerRenderer = this.buildDateSpinnerRenderer();
+		column.setCellRenderer(spinnerRenderer);
+		column.setCellEditor(new TableCellEditorAdapter(this.buildDateSpinnerRenderer()));
+		rowHeight = Math.max(rowHeight, spinnerRenderer.getPreferredHeight());
+
+		// eye color column (combo-box)
+		// the jdk combo-box renderer looks like a text field
+		// until the user starts an edit - use a custom one
+		column = table.getColumnModel().getColumn(PersonColumnAdapter.EYE_COLOR_COLUMN);
+		ComboBoxTableCellRenderer eyeColorRenderer = this.buildEyeColorComboBoxRenderer();
+		column.setCellRenderer(eyeColorRenderer);
+		column.setCellEditor(new TableCellEditorAdapter(this.buildEyeColorComboBoxRenderer()));
+		rowHeight = Math.max(rowHeight, eyeColorRenderer.getPreferredHeight());
+
+		// evil (check box)
+		// the jdk check box renderer and editor suck - use a custom ones
+		column = table.getColumnModel().getColumn(PersonColumnAdapter.EVIL_COLUMN);
+		CheckBoxTableCellRenderer evilRenderer = new CheckBoxTableCellRenderer();
+		column.setCellRenderer(evilRenderer);
+		column.setCellEditor(new TableCellEditorAdapter(new CheckBoxTableCellRenderer()));
+		rowHeight = Math.max(rowHeight, evilRenderer.getPreferredHeight());
+
+		// adventure count column (spinner)
+		column = table.getColumnModel().getColumn(PersonColumnAdapter.ADVENTURE_COUNT_COLUMN);
+		spinnerRenderer = this.buildNumberSpinnerRenderer();
+		column.setCellRenderer(spinnerRenderer);
+		column.setCellEditor(new TableCellEditorAdapter(this.buildNumberSpinnerRenderer()));
+		rowHeight = Math.max(rowHeight, spinnerRenderer.getPreferredHeight());
+
+		table.setRowHeight(rowHeight);
+		return table;
+	}
+
+	private SpinnerTableCellRenderer buildDateSpinnerRenderer() {
+		return new SpinnerTableCellRenderer(new DateSpinnerModelAdapter(new SimplePropertyValueModel()));
+	}
+
+	private SpinnerTableCellRenderer buildNumberSpinnerRenderer() {
+		return new SpinnerTableCellRenderer(new NumberSpinnerModelAdapter(new SimplePropertyValueModel()));
+	}
+
+	private ComboBoxTableCellRenderer buildEyeColorComboBoxRenderer() {
+		return new ComboBoxTableCellRenderer(this.buildReadOnlyEyeColorComboBoxModel(), this.buildEyeColorRenderer());
+	}
+
+	private ComboBoxModel buildReadOnlyEyeColorComboBoxModel() {
+		return new ComboBoxModelAdapter(this.eyeColorListHolder, new SimplePropertyValueModel());
+	}
+
+	private ListCellRenderer buildEyeColorRenderer() {
+		return new EyeColorRenderer();
+	}
+
+	private Component buildControlPanel() {
+		JPanel controlPanel = new JPanel(new GridLayout(0, 1));
+		controlPanel.add(this.buildButtonPanel());
+		controlPanel.add(this.buildPersonPanel());
+		return controlPanel;
+	}
+
+	private Component buildButtonPanel() {
+		JPanel buttonPanel = new JPanel(new GridLayout(1, 0));
+		buttonPanel.add(this.buildAddButton());
+		buttonPanel.add(this.buildRemoveButton());
+		buttonPanel.add(this.buildRenameButton());
+		buttonPanel.add(this.buildAddEyeColorButton());
+		buttonPanel.add(this.buildPrintButton());
+		buttonPanel.add(this.buildResetButton());
+		return buttonPanel;
+	}
+
+	private Component buildPersonPanel() {
+		JPanel personPanel = new JPanel(new GridLayout(1, 0));
+		personPanel.add(this.buildNameTextField());
+		personPanel.add(this.buildBirthDateSpinner());
+		personPanel.add(this.buildGoneWestDateSpinner());
+		personPanel.add(this.buildEyeColorComboBox());
+		personPanel.add(this.buildEvilCheckBox());
+		personPanel.add(this.buildRankSpinner());
+		personPanel.add(this.buildAdventureCountSpinner());
+		return personPanel;
+	}
+
+
+	// ********** add button **********
+
+	private JButton buildAddButton() {
+		return new JButton(this.buildAddAction());
+	}
+
+	private Action buildAddAction() {
+		Action action = new AbstractAction("add") {
+			public void actionPerformed(ActionEvent event) {
+				TableModelAdapterUITest.this.addPerson();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void addPerson() {
+		String name = this.getNameFromUser();
+		if (name != null) {
+			this.setSelectedPerson(this.crowd().addPerson(name));
+		}
+	}
+
+
+	// ********** remove button **********
+
+	private JButton buildRemoveButton() {
+		return new JButton(this.buildRemoveAction());
+	}
+
+	private Action buildRemoveAction() {
+		this.removeAction = new AbstractAction("remove") {
+			public void actionPerformed(ActionEvent event) {
+				TableModelAdapterUITest.this.removePerson();
+			}
+		};
+		this.removeAction.setEnabled(false);
+		return this.removeAction;
+	}
+
+	void removePerson() {
+		Person person = this.selectedPerson();
+		if (person != null) {
+			this.crowd().removePerson(person);
+		}
+	}
+
+
+	// ********** rename button **********
+
+	private JButton buildRenameButton() {
+		return new JButton(this.buildRenameAction());
+	}
+
+	private Action buildRenameAction() {
+		this.renameAction = new AbstractAction("rename") {
+			public void actionPerformed(ActionEvent event) {
+				TableModelAdapterUITest.this.renamePerson();
+			}
+		};
+		this.renameAction.setEnabled(false);
+		return this.renameAction;
+	}
+
+	void renamePerson() {
+		Person person = this.selectedPerson();
+		if (person != null) {
+			String name = this.promptUserForName(person.getName());
+			if (name != null) {
+				person.setName(name);
+				this.setSelectedPerson(person);
+			}
+		}
+	}
+
+
+	// ********** add eye color button **********
+
+	private JButton buildAddEyeColorButton() {
+		return new JButton(this.buildAddEyeColorAction());
+	}
+
+	private Action buildAddEyeColorAction() {
+		Action action = new AbstractAction("add eye color") {
+			public void actionPerformed(ActionEvent event) {
+				TableModelAdapterUITest.this.addEyeColor();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void addEyeColor() {
+		String color = this.promptUserForEyeColor();
+		if (color != null) {
+			this.eyeColorListHolder.addItem(color);
+		}
+	}
+
+	private String promptUserForEyeColor() {
+		while (true) {
+			String eyeColor = JOptionPane.showInputDialog("Eye Color");
+			if (eyeColor == null) {
+				return null;		// user pressed <Cancel>
+			}
+			if ((eyeColor.length() == 0)) {
+				JOptionPane.showMessageDialog(null, "The eye color is required.", "Invalid Eye Color", JOptionPane.ERROR_MESSAGE);
+			} else if (CollectionTools.contains((Iterator) this.eyeColorListHolder.getValue(), eyeColor)) {
+				JOptionPane.showMessageDialog(null, "The eye color already exists.", "Invalid Eye Color", JOptionPane.ERROR_MESSAGE);
+			} else {
+				return eyeColor;
+			}
+		}
+	}
+
+
+	// ********** print button **********
+
+	private JButton buildPrintButton() {
+		return new JButton(this.buildPrintAction());
+	}
+
+	private Action buildPrintAction() {
+		Action action = new AbstractAction("print") {
+			public void actionPerformed(ActionEvent event) {
+				TableModelAdapterUITest.this.printCrowd();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void printCrowd() {
+		System.out.println(this.crowd());
+		for (Iterator<Person> stream = this.crowd().people(); stream.hasNext(); ) {
+			System.out.println("\t" + stream.next());
+		}
+	}
+
+
+	// ********** reset button **********
+
+	private JButton buildResetButton() {
+		return new JButton(this.buildResetAction());
+	}
+
+	private Action buildResetAction() {
+		Action action = new AbstractAction("reset") {
+			public void actionPerformed(ActionEvent event) {
+				TableModelAdapterUITest.this.reset();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	void reset() {
+		this.crowdHolder.setValue(this.buildCrowd());
+	}
+
+
+	// ********** new name dialog **********
+
+	private String getNameFromUser() {
+		return this.promptUserForName(null);
+	}
+
+	private String promptUserForName(String originalName) {
+		while (true) {
+			String name = JOptionPane.showInputDialog("Person Name");
+			if (name == null) {
+				return null;		// user pressed <Cancel>
+			}
+			if ((name.length() == 0)) {
+				JOptionPane.showMessageDialog(null, "The name is required.", "Invalid Name", JOptionPane.ERROR_MESSAGE);
+			} else if (CollectionTools.contains(this.crowd().peopleNames(), name)) {
+				JOptionPane.showMessageDialog(null, "The name already exists.", "Invalid Name", JOptionPane.ERROR_MESSAGE);
+			} else {
+				return name;
+			}
+		}
+	}
+
+
+	// ********** name text field **********
+
+	private Component buildNameTextField() {
+		JTextField textField = new JTextField(this.buildNameDocument(), null, 0);
+		textField.setEditable(false);
+		return textField;
+	}
+
+	private Document buildNameDocument() {
+		return new DocumentAdapter(this.buildNameAdapter());
+	}
+
+	private PropertyValueModel buildNameAdapter() {
+		return new PropertyAspectAdapter(this.selectedPersonHolder, Person.NAME_PROPERTY) {
+			@Override
+			protected Object getValueFromSubject() {
+				return ((Person) this.subject).getName();
+			}
+			@Override
+			protected void setValueOnSubject(Object value) {
+				((Person) this.subject).setName((String) value);
+			}
+		};
+	}
+
+
+	// ********** birth date spinner **********
+
+	private JSpinner buildBirthDateSpinner() {
+		return new JSpinner(this.buildBirthDateSpinnerModel());
+	}
+
+	private SpinnerModel buildBirthDateSpinnerModel() {
+		return new DateSpinnerModelAdapter(this.buildBirthDateAdapter());
+	}
+
+	private PropertyValueModel buildBirthDateAdapter() {
+		return new PropertyAspectAdapter(this.selectedPersonHolder, Person.BIRTH_DATE_PROPERTY) {
+			@Override
+			protected Object getValueFromSubject() {
+				return ((Person) this.subject).getBirthDate();
+			}
+			@Override
+			protected void setValueOnSubject(Object value) {
+				((Person) this.subject).setBirthDate((Date) value);
+			}
+		};
+	}
+
+
+	// ********** gone west date spinner **********
+
+	private JSpinner buildGoneWestDateSpinner() {
+		return new JSpinner(this.buildGoneWestDateSpinnerModel());
+	}
+
+	private SpinnerModel buildGoneWestDateSpinnerModel() {
+		return new DateSpinnerModelAdapter(this.buildGoneWestDateAdapter());
+	}
+
+	private PropertyValueModel buildGoneWestDateAdapter() {
+		return new PropertyAspectAdapter(this.selectedPersonHolder, Person.GONE_WEST_DATE_PROPERTY) {
+			@Override
+			protected Object getValueFromSubject() {
+				return ((Person) this.subject).getGoneWestDate();
+			}
+			@Override
+			protected void setValueOnSubject(Object value) {
+				((Person) this.subject).setGoneWestDate((Date) value);
+			}
+		};
+	}
+
+
+	// ********** eye color combo-box **********
+
+	private JComboBox buildEyeColorComboBox() {
+		return new JComboBox(this.buildEyeColorComboBoxModel());
+	}
+
+	private ComboBoxModel buildEyeColorComboBoxModel() {
+		return new ComboBoxModelAdapter(this.eyeColorListHolder, this.buildEyeColorAdapter());
+	}
+
+	private PropertyValueModel buildEyeColorAdapter() {
+		return new PropertyAspectAdapter(this.selectedPersonHolder, Person.EYE_COLOR_PROPERTY) {
+			@Override
+			protected Object getValueFromSubject() {
+				return ((Person) this.subject).getEyeColor();
+			}
+			@Override
+			protected void setValueOnSubject(Object value) {
+				((Person) this.subject).setEyeColor((String) value);
+			}
+		};
+	}
+
+
+	// ********** evil check box **********
+
+	private JCheckBox buildEvilCheckBox() {
+		JCheckBox checkBox = new JCheckBox();
+		checkBox.setText("evil");
+		checkBox.setModel(this.buildEvilCheckBoxModel());
+		return checkBox;
+	}
+
+	private ButtonModel buildEvilCheckBoxModel() {
+		return new CheckBoxModelAdapter(this.buildEvilAdapter());
+	}
+
+	private PropertyValueModel buildEvilAdapter() {
+		return new PropertyAspectAdapter(this.selectedPersonHolder, Person.EVIL_PROPERTY) {
+			@Override
+			protected Object getValueFromSubject() {
+				return Boolean.valueOf(((Person) this.subject).isEvil());
+			}
+			@Override
+			protected void setValueOnSubject(Object value) {
+				((Person) this.subject).setEvil(((Boolean) value).booleanValue());
+			}
+		};
+	}
+
+
+	// ********** rank spinner **********
+
+	private JSpinner buildRankSpinner() {
+		return new JSpinner(this.buildRankSpinnerModel());
+	}
+
+	private SpinnerModel buildRankSpinnerModel() {
+		return new NumberSpinnerModelAdapter(this.buildRankAdapter());
+	}
+
+	private PropertyValueModel buildRankAdapter() {
+		return new PropertyAspectAdapter(this.selectedPersonHolder, Person.RANK_PROPERTY) {
+			@Override
+			protected Object getValueFromSubject() {
+				return new Integer(((Person) this.subject).getRank());
+			}
+			@Override
+			protected void setValueOnSubject(Object value) {
+				((Person) this.subject).setRank(((Integer) value).intValue());
+			}
+		};
+	}
+
+
+	// ********** adventure count spinner **********
+
+	private JSpinner buildAdventureCountSpinner() {
+		return new JSpinner(this.buildAdventureCountSpinnerModel());
+	}
+
+	private SpinnerModel buildAdventureCountSpinnerModel() {
+		return new NumberSpinnerModelAdapter(this.buildAdventureCountAdapter());
+	}
+
+	private PropertyValueModel buildAdventureCountAdapter() {
+		return new PropertyAspectAdapter(this.selectedPersonHolder, Person.ADVENTURE_COUNT_PROPERTY) {
+			@Override
+			protected Object getValueFromSubject() {
+				return new Integer(((Person) this.subject).getAdventureCount());
+			}
+			@Override
+			protected void setValueOnSubject(Object value) {
+				((Person) this.subject).setAdventureCount(((Integer) value).intValue());
+			}
+		};
+	}
+
+
+	// ********** queries **********
+
+	private Crowd crowd() {
+		return (Crowd) this.crowdHolder.getValue();
+	}
+
+	private Person selectedPerson() {
+		if (this.rowSelectionModel.isSelectionEmpty()) {
+			return null;
+		}
+		return (Person) this.rowSelectionModel.getSelectedValue();
+	}
+
+	private void setSelectedPerson(Person person) {
+		this.rowSelectionModel.setSelectedValue(person);
+	}
+
+
+	// ********** custom renderer **********
+	
+	/**
+	 * This is simply an example of a renderer for the embedded combo-box.
+	 * It does nothing special unless you uncomment the code below....
+	 */
+	private class EyeColorRenderer extends DefaultListCellRenderer {
+		EyeColorRenderer() {
+			super();
+		}
+		@Override
+		public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
+			// just do something to show the renderer is working...
+	//		value = ">" + value;
+			return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+		}
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/TreeModelAdapterTests.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/TreeModelAdapterTests.java
new file mode 100644
index 0000000..d70e7a7
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/TreeModelAdapterTests.java
@@ -0,0 +1,831 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Iterator;
+
+import javax.swing.Icon;
+import javax.swing.JTree;
+import javax.swing.event.TreeModelEvent;
+import javax.swing.event.TreeModelListener;
+import javax.swing.tree.TreeModel;
+
+import org.eclipse.jpt.utility.internal.HashBag;
+import org.eclipse.jpt.utility.internal.IndentingPrintWriter;
+import org.eclipse.jpt.utility.internal.iterators.ReadOnlyIterator;
+import org.eclipse.jpt.utility.internal.model.AbstractModel;
+import org.eclipse.jpt.utility.internal.model.event.PropertyChangeEvent;
+import org.eclipse.jpt.utility.internal.model.listener.PropertyChangeListener;
+import org.eclipse.jpt.utility.internal.model.value.AbstractTreeNodeValueModel;
+import org.eclipse.jpt.utility.internal.model.value.CollectionAspectAdapter;
+import org.eclipse.jpt.utility.internal.model.value.CollectionValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ItemPropertyListValueModelAdapter;
+import org.eclipse.jpt.utility.internal.model.value.ListValueModel;
+import org.eclipse.jpt.utility.internal.model.value.NullListValueModel;
+import org.eclipse.jpt.utility.internal.model.value.PropertyAspectAdapter;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimpleListValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimplePropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SortedListValueModelAdapter;
+import org.eclipse.jpt.utility.internal.model.value.TransformationListValueModelAdapter;
+import org.eclipse.jpt.utility.internal.model.value.TreeNodeValueModel;
+import org.eclipse.jpt.utility.internal.model.value.ValueModel;
+import org.eclipse.jpt.utility.internal.model.value.swing.TreeModelAdapter;
+import org.eclipse.jpt.utility.internal.swing.Displayable;
+
+import junit.framework.TestCase;
+
+/**
+ * 
+ */
+public class TreeModelAdapterTests extends TestCase {
+	boolean eventFired;
+
+	public TreeModelAdapterTests(String name) {
+		super(name);
+	}
+
+	public void testGetRoot() {
+		TreeModel treeModel = this.buildSortedTreeModel();
+		treeModel.addTreeModelListener(new TestTreeModelListener());
+		TestNode rootNode = (TestNode) treeModel.getRoot();
+		TestModel root = rootNode.getTestModel();
+		assertEquals("root", root.getName());
+//		root.dump();
+//		rootNode.dump();
+	}
+
+	public void testGetChild() {
+		TreeModel treeModel = this.buildSortedTreeModel();
+		treeModel.addTreeModelListener(new TestTreeModelListener());
+		TestNode rootNode = (TestNode) treeModel.getRoot();
+
+		TestNode expected = rootNode.childNamed("node 1");
+		TestNode actual = (TestNode) treeModel.getChild(rootNode, 1);
+		assertEquals(expected, actual);
+
+		expected = rootNode.childNamed("node 2");
+		actual = (TestNode) treeModel.getChild(rootNode, 2);
+		assertEquals(expected, actual);
+	}
+
+	public void testGetChildCount() {
+		TreeModel treeModel = this.buildSortedTreeModel();
+		treeModel.addTreeModelListener(new TestTreeModelListener());
+		TestNode rootNode = (TestNode) treeModel.getRoot();
+
+		assertEquals(5, treeModel.getChildCount(rootNode));
+
+		TestNode node = rootNode.childNamed("node 1");
+		assertEquals(1, treeModel.getChildCount(node));
+	}
+
+	public void testGetIndexOfChild() {
+		TreeModel treeModel = this.buildSortedTreeModel();
+		treeModel.addTreeModelListener(new TestTreeModelListener());
+		TestNode rootNode = (TestNode) treeModel.getRoot();
+
+		TestNode child = rootNode.childNamed("node 0");
+		assertEquals(0, treeModel.getIndexOfChild(rootNode, child));
+
+		child = rootNode.childNamed("node 1");
+		assertEquals(1, treeModel.getIndexOfChild(rootNode, child));
+
+		child = rootNode.childNamed("node 2");
+		assertEquals(2, treeModel.getIndexOfChild(rootNode, child));
+		TestNode grandchild = child.childNamed("node 2.2");
+		assertEquals(2, treeModel.getIndexOfChild(child, grandchild));
+	}
+
+	public void testIsLeaf() {
+		TreeModel treeModel = this.buildSortedTreeModel();
+		treeModel.addTreeModelListener(new TestTreeModelListener());
+		TestNode rootNode = (TestNode) treeModel.getRoot();
+		assertFalse(treeModel.isLeaf(rootNode));
+		TestNode node = rootNode.childNamed("node 1");
+		assertFalse(treeModel.isLeaf(node));
+		node = rootNode.childNamed("node 3");
+		assertTrue(treeModel.isLeaf(node));
+	}
+
+
+	public void testTreeNodesChanged() {
+		// the only way to trigger a "node changed" event is to use an unsorted tree;
+		// a sorted tree will will trigger only "node removed" and "node inserted" events
+		TreeModel treeModel = this.buildUnsortedTreeModel();
+		this.eventFired = false;
+		treeModel.addTreeModelListener(new TestTreeModelListener() {
+			@Override
+			public void treeNodesChanged(TreeModelEvent e) {
+				TreeModelAdapterTests.this.eventFired = true;
+			}
+		});
+		TestNode rootNode = (TestNode) treeModel.getRoot();
+		TestNode node = rootNode.childNamed("node 1");
+		TestModel tm = node.getTestModel();
+		tm.setName("node 1++");
+		assertTrue(this.eventFired);
+
+		this.eventFired = false;
+		node = node.childNamed("node 1.1");
+		tm = node.getTestModel();
+		tm.setName("node 1.1++");
+		assertTrue(this.eventFired);
+	}
+
+	public void testTreeNodesInserted() {
+		// use an unsorted tree so the nodes are not re-shuffled...
+		TreeModel treeModel = this.buildUnsortedTreeModel();
+		this.eventFired = false;
+		treeModel.addTreeModelListener(new TestTreeModelListener() {
+			@Override
+			public void treeNodesInserted(TreeModelEvent e) {
+				TreeModelAdapterTests.this.eventFired = true;
+			}
+		});
+		TestNode rootNode = (TestNode) treeModel.getRoot();
+		TestNode node = rootNode.childNamed("node 1");
+		TestModel tm = node.getTestModel();
+		tm.addChild("new child...");
+		assertTrue(this.eventFired);
+
+		this.eventFired = false;
+		node = node.childNamed("node 1.1");
+		tm = node.getTestModel();
+		tm.addChild("another new child...");
+		assertTrue(this.eventFired);
+	}
+
+	public void testTreeNodesRemoved() {
+		TreeModel treeModel = this.buildUnsortedTreeModel();
+		this.eventFired = false;
+		treeModel.addTreeModelListener(new TestTreeModelListener() {
+			@Override
+			public void treeNodesRemoved(TreeModelEvent e) {
+				TreeModelAdapterTests.this.eventFired = true;
+			}
+		});
+		TestNode rootNode = (TestNode) treeModel.getRoot();
+		TestModel root = rootNode.getTestModel();
+		root.removeChild(root.childNamed("node 3"));
+		assertTrue(this.eventFired);
+
+		this.eventFired = false;
+		TestNode node = rootNode.childNamed("node 2");
+		TestModel tm = node.getTestModel();
+		tm.removeChild(tm.childNamed("node 2.2"));
+		assertTrue(this.eventFired);
+	}
+
+	public void testTreeStructureChanged() {
+		PropertyValueModel nodeHolder = new SimplePropertyValueModel(this.buildSortedRootNode());
+		TreeModel treeModel = new TreeModelAdapter(nodeHolder);
+		this.eventFired = false;
+		treeModel.addTreeModelListener(new TestTreeModelListener() {
+			@Override
+			public void treeNodesInserted(TreeModelEvent e) {
+				// do nothing
+			}
+			@Override
+			public void treeNodesRemoved(TreeModelEvent e) {
+				// do nothing
+			}
+			@Override
+			public void treeStructureChanged(TreeModelEvent e) {
+				TreeModelAdapterTests.this.eventFired = true;
+			}
+		});
+		nodeHolder.setValue(this.buildUnsortedRootNode());
+		assertTrue(this.eventFired);
+	}
+
+	/**
+	 * test a problem we had where removing a child from a tree would cause
+	 * the JTree to call #equals(Object) on each node removed (actually, it was
+	 * TreePath, but that was because its own #equals(Object) was called by
+	 * JTree); and since we had already removed the last listener from the
+	 * aspect adapter, the aspect adapter would say its value was null; this
+	 * would cause a NPE until we tweaked TreeModelAdapter to remove its
+	 * listeners from a node only *after* the node had been completely
+	 * removed from the JTree
+	 * @see TreeModelAdapter#removeNode(Object[], int, TreeNodeValueModel)
+	 * @see TreeModelAdapter#addNode(Object[], int, TreeNodeValueModel)
+	 */
+	public void testLazyInitialization() {
+		TreeModel treeModel = this.buildSpecialTreeModel();
+		JTree jTree = new JTree(treeModel);
+		TestNode rootNode = (TestNode) treeModel.getRoot();
+		TestModel root = rootNode.getTestModel();
+		// this would cause a NPE:
+		root.removeChild(root.childNamed("node 3"));
+		assertEquals(treeModel, jTree.getModel());
+	}
+
+
+	private TreeModel buildSortedTreeModel() {
+		return new TreeModelAdapter(this.buildSortedRootNode());
+	}
+
+	private TestNode buildSortedRootNode() {
+		return new SortedTestNode(this.buildRoot());
+	}
+
+	private TreeModel buildUnsortedTreeModel() {
+		return new TreeModelAdapter(this.buildUnsortedRootNode());
+	}
+
+	private TestNode buildUnsortedRootNode() {
+		return new UnsortedTestNode(this.buildRoot());
+	}
+
+	private TreeModel buildSpecialTreeModel() {
+		return new TreeModelAdapter(this.buildSpecialRootNode());
+	}
+
+	private TestNode buildSpecialRootNode() {
+		return new SpecialTestNode(this.buildRoot());
+	}
+
+	private TestModel buildRoot() {
+		TestModel root = new TestModel("root");
+		/*Node node_0 = */root.addChild("node 0");
+		TestModel node_1 = root.addChild("node 1");
+		TestModel node_1_1 = node_1.addChild("node 1.1");
+		/*Node node_1_1_1 = */node_1_1.addChild("node 1.1.1");
+		TestModel node_2 = root.addChild("node 2");
+		/*Node node_2_0 = */node_2.addChild("node 2.0");
+		/*Node node_2_1 = */node_2.addChild("node 2.1");
+		/*Node node_2_2 = */node_2.addChild("node 2.2");
+		/*Node node_2_3 = */node_2.addChild("node 2.3");
+		/*Node node_2_4 = */node_2.addChild("node 2.4");
+		/*Node node_2_5 = */node_2.addChild("node 2.5");
+		/*Node node_3 = */root.addChild("node 3");
+		/*Node node_4 = */root.addChild("node 4");
+		return root;
+	}
+
+
+	// ********** member classes **********
+
+	/**
+	 * This is a typical model class with the typical change notifications
+	 * for #name and #children.
+	 */
+	public static class TestModel extends AbstractModel {
+
+		// the  parent is immutable; the root's parent is null
+		private TestModel parent;
+
+		// the name is mutable; so I guess it isn't the "primary key" :-)
+		private String name;
+			public static final String NAME_PROPERTY = "name";
+
+		private final Collection<TestModel> children;
+			public static final String CHILDREN_COLLECTION = "children";
+
+
+		public TestModel(String name) {	// root ctor
+			super();
+			this.name = name;
+			this.children = new HashBag<TestModel>();
+		}
+		private TestModel(TestModel parent, String name) {
+			this(name);
+			this.parent = parent;
+		}
+
+		public TestModel getParent() {
+			return this.parent;
+		}
+
+		public String getName() {
+			return this.name;
+		}
+		public void setName(String name) {
+			Object old = this.name;
+			this.name = name;
+			this.firePropertyChanged(NAME_PROPERTY, old, name);
+		}
+
+		public Iterator<TestModel> children() {
+			return new ReadOnlyIterator<TestModel>(this.children);
+		}
+		public int childrenSize() {
+			return this.children.size();
+		}
+		public TestModel addChild(String childName) {
+			TestModel child = new TestModel(this, childName);
+			this.children.add(child);
+			this.fireItemAdded(CHILDREN_COLLECTION, child);
+			return child;
+		}
+		public TestModel[] addChildren(String[] childNames) {
+			TestModel[] newChildren = new TestModel[childNames.length];
+			for (int i = 0; i < childNames.length; i++) {
+				TestModel child = new TestModel(this, childNames[i]);
+				this.children.add(child);
+				newChildren[i] = child;
+			}
+			this.fireItemsAdded(CHILDREN_COLLECTION, Arrays.asList(newChildren));
+			return newChildren;
+		}
+		public void removeChild(TestModel child) {
+			if (this.children.remove(child)) {
+				this.fireItemRemoved(CHILDREN_COLLECTION, child);
+			}
+		}
+		public void removeChildren(TestModel[] testModels) {
+			Collection<TestModel> removedChildren = new ArrayList<TestModel>();
+			for (int i = 0; i < testModels.length; i++) {
+				if (this.children.remove(testModels[i])) {
+					removedChildren.add(testModels[i]);
+				} else {
+					throw new IllegalArgumentException(String.valueOf(testModels[i]));
+				}
+			}
+			if ( ! removedChildren.isEmpty()) {
+				this.fireItemsRemoved(CHILDREN_COLLECTION, removedChildren);
+			}
+		}
+		public void clearChildren() {
+			this.children.clear();
+			this.fireCollectionChanged(CHILDREN_COLLECTION);
+		}
+		public TestModel childNamed(String childName) {
+			for (Iterator<TestModel> stream = this.children(); stream.hasNext(); ) {
+				TestModel child = stream.next();
+				if (child.getName().equals(childName)) {
+					return child;
+				}
+			}
+			throw new RuntimeException("child not found: " + childName);
+		}
+
+		public String dumpString() {
+			StringWriter sw = new StringWriter();
+			IndentingPrintWriter ipw = new IndentingPrintWriter(sw);
+			this.dumpOn(ipw);
+			return sw.toString();
+		}
+		public void dumpOn(IndentingPrintWriter writer) {
+			writer.println(this);
+			writer.indent();
+			for (Iterator<TestModel> stream = this.children(); stream.hasNext(); ) {
+				stream.next().dumpOn(writer);
+			}
+			writer.undent();
+		}
+		public void dumpOn(OutputStream stream) {
+			IndentingPrintWriter writer = new IndentingPrintWriter(new OutputStreamWriter(stream));
+			this.dumpOn(writer);
+			writer.flush();
+		}
+		public void dump() {
+			this.dumpOn(System.out);
+		}
+
+		@Override
+		public String toString() {
+			return "TestModel(" + this.name + ")";
+		}
+
+	}
+
+
+	/**
+	 * This Node wraps a TestModel and converts into something that can
+	 * be used by TreeModelAdapter. It converts changes to the TestModel's
+	 * name into "state changes" to the Node; and converts the
+	 * TestModel's children into a ListValueModel of Nodes whose order is
+	 * determined by subclass implementations.
+	 */
+	public static abstract class TestNode extends AbstractTreeNodeValueModel implements Displayable {
+		/** the model object wrapped by this node */
+		private TestModel testModel;
+		/** this node's parent node; null for the root node */
+		private TestNode parent;
+		/** this node's child nodes */
+		private ListValueModel childrenModel;
+		/** a listener that notifies us when the model object's "internal state" changes */
+		private PropertyChangeListener testModelListener;
+
+
+		// ********** constructors **********
+
+		/**
+		 * root node constructor
+		 */
+		public TestNode(TestModel testModel) {
+			this(null, testModel);
+		}
+
+		/**
+		 * branch or leaf node constructor
+		 */
+		public TestNode(TestNode parent, TestModel testModel) {
+			super();
+			this.initialize(parent, testModel);
+		}
+
+
+		// ********** initialization **********
+
+		@Override
+		protected void initialize() {
+			super.initialize();
+			this.testModelListener = this.buildTestModelListener();
+		}
+
+		private PropertyChangeListener buildTestModelListener() {
+			return new PropertyChangeListener() {
+				public void propertyChanged(PropertyChangeEvent e) {
+					TestNode.this.testModelChanged(e);
+				}
+			};
+		}
+
+		protected void initialize(TestNode p, TestModel tm) {
+			this.parent = p;
+			this.testModel = tm;
+			this.childrenModel = this.buildChildrenModel(tm);
+		}
+
+		/**
+		 * subclasses decide the order of the child nodes
+		 */
+		protected abstract ListValueModel buildChildrenModel(TestModel model);
+
+		/**
+		 * used by subclasses;
+		 * transform the test model children into nodes
+		 */
+		protected ListValueModel buildNodeAdapter(TestModel model) {
+			return new TransformationListValueModelAdapter(this.buildChildrenAdapter(model)) {
+				@Override
+				protected Object transformItem(Object item) {
+					return TestNode.this.buildChildNode((TestModel) item);
+				}
+			};
+		}
+
+		/**
+		 * subclasses must build a concrete node for the specified test model
+		 */
+		protected abstract TestNode buildChildNode(TestModel childTestModel);
+
+		/**
+		 * return a collection value model on the specified model's children
+		 */
+		protected CollectionValueModel buildChildrenAdapter(TestModel model) {
+			return new CollectionAspectAdapter(TestModel.CHILDREN_COLLECTION, model) {
+				@Override
+				protected Iterator getValueFromSubject() {
+					return ((TestModel) this.subject).children();
+				}
+				@Override
+				protected int sizeFromSubject() {
+					return ((TestModel) this.subject).childrenSize();
+				}
+			};
+		}
+
+
+		// ********** TreeNodeValueModel implementation **********
+
+		public Object getValue() {
+			return this.testModel;
+		}
+
+		/**
+		 * this will probably never be called...
+		 */
+		@Override
+		public void setValue(Object value) {
+			Object old = this.testModel;
+			this.testModel = (TestModel) value;
+			this.firePropertyChanged(VALUE, old, this.testModel);
+		}
+
+		public TreeNodeValueModel getParent() {
+			return this.parent;
+		}
+
+		public ListValueModel getChildrenModel() {
+			return this.childrenModel;
+		}
+
+
+		// ********** AbstractTreeNodeValueModel implementation **********
+
+		@Override
+		protected void engageValue() {
+			this.testModel.addPropertyChangeListener(TestModel.NAME_PROPERTY, this.testModelListener);
+		}
+
+		@Override
+		protected void disengageValue() {
+			this.testModel.removePropertyChangeListener(TestModel.NAME_PROPERTY, this.testModelListener);
+		}
+
+
+		// ********** Displayable implementation **********
+
+		public String displayString() {
+			return this.testModel.getName();
+		}
+
+		public Icon icon() {
+			return null;
+		}
+
+
+		// ********** debugging support **********
+
+		public String dumpString() {
+			StringWriter sw = new StringWriter();
+			IndentingPrintWriter ipw = new IndentingPrintWriter(sw);
+			this.dumpOn(ipw);
+			return sw.toString();
+		}
+
+		public void dumpOn(IndentingPrintWriter writer) {
+			writer.println(this);
+			writer.indent();
+			for (Iterator stream = (Iterator) this.childrenModel.getValue(); stream.hasNext(); ) {
+				((TestNode) stream.next()).dumpOn(writer);
+			}
+			writer.undent();
+		}
+
+		public void dumpOn(OutputStream stream) {
+			IndentingPrintWriter writer = new IndentingPrintWriter(new OutputStreamWriter(stream));
+			this.dumpOn(writer);
+			writer.flush();
+		}
+
+		public void dump() {
+			this.dumpOn(System.out);
+		}
+
+
+		// ********** behavior **********
+
+		/**
+		 * the model's name has changed, forward the event to our listeners
+		 */
+		protected void testModelChanged(PropertyChangeEvent e) {
+			// we need to notify listeners that our "internal state" has changed
+			this.fireStateChanged();
+			// our display string stays in synch with the model's name
+			this.firePropertyChanged(DISPLAY_STRING_PROPERTY, e.oldValue(), e.newValue());
+		}
+
+
+		// ********** queries **********
+
+		public TestModel getTestModel() {
+			return this.testModel;
+		}
+
+		/**
+		 * testing convenience method
+		 */
+		public TestNode childNamed(String name) {
+			for (Iterator stream = (Iterator) this.childrenModel.getValue(); stream.hasNext(); ) {
+				TestNode childNode = (TestNode) stream.next();
+				if (childNode.getTestModel().getName().equals(name)) {
+					return childNode;
+				}
+			}
+			throw new IllegalArgumentException("child not found: " + name);
+		}
+
+
+		// ********** standard methods **********
+
+		/**
+		 * use the standard Displayable comparator
+		 */
+		public int compareTo(Displayable d) {
+			return DEFAULT_COMPARATOR.compare(this, d);
+		}
+
+		@Override
+		public String toString() {
+			return "Node(" + this.testModel + ")";
+		}
+
+	}
+
+	/**
+	 * concrete implementation that keeps its children sorted
+	 */
+	public static class SortedTestNode extends TestNode {
+
+		// ********** constructors **********
+		public SortedTestNode(TestModel testModel) {
+			super(testModel);
+		}
+		public SortedTestNode(TestNode parent, TestModel testModel) {
+			super(parent, testModel);
+		}
+
+		// ********** initialization **********
+		/** the list should be sorted */
+		@Override
+		protected ListValueModel buildChildrenModel(TestModel testModel) {
+			return new SortedListValueModelAdapter(this.buildDisplayStringAdapter(testModel));
+		}
+		/** the display string (name) of each node can change */
+		protected ListValueModel buildDisplayStringAdapter(TestModel testModel) {
+			return new ItemPropertyListValueModelAdapter(this.buildNodeAdapter(testModel), DISPLAY_STRING_PROPERTY);
+		}
+		/** children are also sorted nodes */
+		@Override
+		protected TestNode buildChildNode(TestModel childNode) {
+			return new SortedTestNode(this, childNode);
+		}
+
+	}
+
+
+	/**
+	 * concrete implementation that leaves its children unsorted
+	 */
+	public static class UnsortedTestNode extends TestNode {
+
+		// ********** constructors **********
+		public UnsortedTestNode(TestModel testModel) {
+			super(testModel);
+		}
+		public UnsortedTestNode(TestNode parent, TestModel testModel) {
+			super(parent, testModel);
+		}
+
+		// ********** initialization **********
+		/** the list should NOT be sorted */
+		@Override
+		protected ListValueModel buildChildrenModel(TestModel testModel) {
+			return this.buildNodeAdapter(testModel);
+		}
+		/** children are also unsorted nodes */
+		@Override
+		protected TestNode buildChildNode(TestModel childNode) {
+			return new UnsortedTestNode(this, childNode);
+		}
+
+	}
+
+
+	/**
+	 * concrete implementation that leaves its children unsorted
+	 * and has a special set of children for "node 3"
+	 */
+	public static class SpecialTestNode extends UnsortedTestNode {
+
+		// ********** constructors **********
+		public SpecialTestNode(TestModel testModel) {
+			super(testModel);
+		}
+		public SpecialTestNode(TestNode parent, TestModel testModel) {
+			super(parent, testModel);
+		}
+
+		// ********** initialization **********
+		/** return a different list of children for "node 3" */
+		@Override
+		protected ListValueModel buildChildrenModel(TestModel testModel) {
+			if (testModel.getName().equals("node 3")) {
+				return this.buildSpecialChildrenModel(testModel);
+			}
+			return super.buildChildrenModel(testModel);
+		}
+		protected ListValueModel buildSpecialChildrenModel(TestModel testModel) {
+			Object[] children = new Object[1];
+			children[0] = new NameTestNode(this);
+			return new SimpleListValueModel(Arrays.asList(children));
+		}
+		/** children are also special nodes */
+		@Override
+		protected TestNode buildChildNode(TestModel childNode) {
+			return new SpecialTestNode(this, childNode);
+		}
+
+	}
+
+
+	public static class NameTestNode extends AbstractTreeNodeValueModel {
+		private PropertyValueModel nameAdapter;
+		private SpecialTestNode specialNode;		// parent node
+		private PropertyChangeListener nameListener;
+
+		// ********** construction/initialization **********
+
+		public NameTestNode(SpecialTestNode specialNode) {
+			super();
+			this.initialize(specialNode);
+		}
+		@Override
+		protected void initialize() {
+			super.initialize();
+			this.nameListener = this.buildNameListener();
+		}
+		protected PropertyChangeListener buildNameListener() {
+			return new PropertyChangeListener() {
+				public void propertyChanged(PropertyChangeEvent e) {
+					NameTestNode.this.nameChanged(e);
+				}
+			};
+		}
+		protected void initialize(SpecialTestNode node) {
+			this.specialNode = node;
+			this.nameAdapter = this.buildNameAdapter();
+		}
+
+		protected PropertyValueModel buildNameAdapter() {
+			return new PropertyAspectAdapter(TestModel.NAME_PROPERTY, this.getTestModel()) {
+				@Override
+				protected Object getValueFromSubject() {
+					return ((TestModel) this.subject).getName();
+				}
+				@Override
+				protected void setValueOnSubject(Object value) {
+					((TestModel) this.subject).setName((String) value);
+				}
+			};
+		}
+
+		public TestModel getTestModel() {
+			return this.specialNode.getTestModel();
+		}
+
+		// ********** TreeNodeValueModel implementation **********
+
+		public Object getValue() {
+			return this.nameAdapter.getValue();
+		}
+		@Override
+		public void setValue(Object value) {
+			this.nameAdapter.setValue(value);
+		}
+		public TreeNodeValueModel getParent() {
+			return this.specialNode;
+		}
+		public ListValueModel getChildrenModel() {
+			return NullListValueModel.instance();
+		}
+
+		// ********** AbstractTreeNodeValueModel implementation **********
+
+		@Override
+		protected void engageValue() {
+			this.nameAdapter.addPropertyChangeListener(ValueModel.VALUE, this.nameListener);
+		}
+		@Override
+		protected void disengageValue() {
+			this.nameAdapter.removePropertyChangeListener(ValueModel.VALUE, this.nameListener);
+		}
+
+		// ********** behavior **********
+
+		protected void nameChanged(PropertyChangeEvent e) {
+			// we need to notify listeners that our "value" has changed
+			this.firePropertyChanged(VALUE, e.oldValue(), e.newValue());
+		}
+	}
+
+
+
+	/**
+	 * listener that will blow up with any event;
+	 * override and implement expected event methods
+	 */
+	public class TestTreeModelListener implements TreeModelListener {
+		public void treeNodesChanged(TreeModelEvent e) {
+			fail("unexpected event");
+		}
+		public void treeNodesInserted(TreeModelEvent e) {
+			fail("unexpected event");
+		}
+		public void treeNodesRemoved(TreeModelEvent e) {
+			fail("unexpected event");
+		}
+		public void treeStructureChanged(TreeModelEvent e) {
+			fail("unexpected event");
+		}
+	}
+
+}
diff --git a/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/TreeModelAdapterUITest.java b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/TreeModelAdapterUITest.java
new file mode 100644
index 0000000..330ed6f
--- /dev/null
+++ b/jpa/tests/org.eclipse.jpt.utility.tests/src/org/eclipse/jpt/utility/tests/internal/model/value/swing/TreeModelAdapterUITest.java
@@ -0,0 +1,426 @@
+/*******************************************************************************
+ * Copyright (c) 2007 Oracle. All rights reserved.
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0, which accompanies this distribution
+ * and is available at http://www.eclipse.org/legal/epl-v10.html.
+ * 
+ * Contributors:
+ *     Oracle - initial API and implementation
+ ******************************************************************************/
+package org.eclipse.jpt.utility.tests.internal.model.value.swing;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.GridLayout;
+import java.awt.TextField;
+import java.awt.event.ActionEvent;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.awt.event.WindowListener;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Iterator;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.JButton;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTree;
+import javax.swing.WindowConstants;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.tree.DefaultTreeSelectionModel;
+import javax.swing.tree.TreeModel;
+import javax.swing.tree.TreePath;
+import javax.swing.tree.TreeSelectionModel;
+
+import org.eclipse.jpt.utility.internal.CollectionTools;
+import org.eclipse.jpt.utility.internal.iterators.EnumerationIterator;
+import org.eclipse.jpt.utility.internal.model.value.PropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.SimplePropertyValueModel;
+import org.eclipse.jpt.utility.internal.model.value.swing.TreeModelAdapter;
+import org.eclipse.jpt.utility.internal.swing.Displayable;
+import org.eclipse.jpt.utility.tests.internal.model.value.swing.TreeModelAdapterTests.SortedTestNode;
+import org.eclipse.jpt.utility.tests.internal.model.value.swing.TreeModelAdapterTests.TestModel;
+import org.eclipse.jpt.utility.tests.internal.model.value.swing.TreeModelAdapterTests.TestNode;
+import org.eclipse.jpt.utility.tests.internal.model.value.swing.TreeModelAdapterTests.UnsortedTestNode;
+
+/**
+ * an example UI for testing the TreeModelAdapter
+ */
+public class TreeModelAdapterUITest {
+
+	// hold the tree so we can restore its expansion state
+	private JTree tree;
+	private PropertyValueModel rootNodeHolder;
+	private boolean sorted;
+	private TreeModel treeModel;
+	private TreeSelectionModel treeSelectionModel;
+	private TextField nameTextField;
+
+	public static void main(String[] args) throws Exception {
+		new TreeModelAdapterUITest().exec(args);
+	}
+
+	private TreeModelAdapterUITest() {
+		super();
+	}
+
+	private void exec(String[] args) throws Exception {
+		this.rootNodeHolder = this.buildRootNodeHolder();
+		this.sorted = this.rootNodeHolder.getValue() instanceof SortedTestNode;
+		this.treeModel = this.buildTreeModel();
+		this.treeSelectionModel = this.buildTreeSelectionModel();
+		this.nameTextField = new TextField();
+		this.openWindow();
+	}
+
+	private PropertyValueModel buildRootNodeHolder() {
+		return new SimplePropertyValueModel(this.buildSortedRootNode());
+	}
+
+	private TestNode buildSortedRootNode() {
+		return new SortedTestNode(this.buildRoot());
+	}
+
+	private TestNode buildUnsortedRootNode() {
+		return new UnsortedTestNode(this.buildRoot());
+	}
+
+	private TestModel buildRoot() {
+		TestModel root = new TestModel("root");
+
+		TestModel node_1 = root.addChild("node 1");
+		/*Node node_1_1 = */node_1.addChild("node 1.1");
+
+		TestModel node_2 = root.addChild("node 2");
+		/*Node node_2_1 = */node_2.addChild("node 2.1");
+		TestModel node_2_2 = node_2.addChild("node 2.2");
+		/*Node node_2_2_1 = */node_2_2.addChild("node 2.2.1");
+		/*Node node_2_2_2 = */node_2_2.addChild("node 2.2.2");
+		/*Node node_2_3 = */node_2.addChild("node 2.3");
+		/*Node node_2_4 = */node_2.addChild("node 2.4");
+		/*Node node_2_5 = */node_2.addChild("node 2.5");
+
+		TestModel node_3 = root.addChild("node 3");
+		TestModel node_3_1 = node_3.addChild("node 3.1");
+		TestModel node_3_1_1 = node_3_1.addChild("node 3.1.1");
+		/*Node node_3_1_1_1 = */node_3_1_1.addChild("node 3.1.1.1");
+
+		/*Node node_4 = */root.addChild("node 4");
+
+		return root;
+	}
+
+	private TreeModel buildTreeModel() {
+		return new TreeModelAdapter(this.rootNodeHolder);
+	}
+
+	private TreeSelectionModel buildTreeSelectionModel() {
+		TreeSelectionModel tsm = new DefaultTreeSelectionModel();
+		tsm.addTreeSelectionListener(this.buildTreeSelectionListener());
+		tsm.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
+		return tsm;
+	}
+
+	private TreeSelectionListener buildTreeSelectionListener() {
+		return new TreeSelectionListener() {
+			public void valueChanged(TreeSelectionEvent e) {
+				TreeModelAdapterUITest.this.treeSelectionChanged(e);
+			}
+		};
+	}
+
+	void treeSelectionChanged(TreeSelectionEvent e) {
+		TestModel selectedTestModel = this.selectedTestModel();
+		if (selectedTestModel != null) {
+			this.nameTextField.setText(selectedTestModel.getName());
+		}
+	}
+
+	private void openWindow() {
+		JFrame window = new JFrame(this.getClass().getName());
+		window.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);
+		window.addWindowListener(this.buildWindowListener());
+		window.getContentPane().add(this.buildMainPanel(), "Center");
+		window.setLocation(300, 300);
+		window.setSize(400, 400);
+		window.setVisible(true);
+	}
+
+	private WindowListener buildWindowListener() {
+		return new WindowAdapter() {
+			@Override
+			public void windowClosing(WindowEvent e) {
+				e.getWindow().setVisible(false);
+				System.exit(0);
+			}
+		};
+	}
+
+	private Component buildMainPanel() {
+		JPanel mainPanel = new JPanel(new BorderLayout());
+		mainPanel.add(this.buildTreePane(), BorderLayout.CENTER);
+		mainPanel.add(this.buildControlPanel(), BorderLayout.SOUTH);
+		return mainPanel;
+	}
+
+	private Component buildTreePane() {
+		return new JScrollPane(this.buildTree());
+	}
+
+	private JTree buildTree() {
+		this.tree = new JTree(this.treeModel) {
+			@Override
+			public String convertValueToText(Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
+				return ((Displayable) value).displayString();
+			}
+		};
+		this.tree.setSelectionModel(this.treeSelectionModel);
+		this.tree.setRootVisible(true);
+		this.tree.setShowsRootHandles(true);
+		this.tree.setRowHeight(20);
+		this.tree.setDoubleBuffered(true);
+		return this.tree;
+	}
+
+	private Component buildControlPanel() {
+		JPanel controlPanel = new JPanel(new GridLayout(0, 1));
+		controlPanel.add(this.buildAddRenameNodePanel());
+		controlPanel.add(this.buildMiscPanel());
+		return controlPanel;
+	}
+
+	private Component buildAddRenameNodePanel() {
+		JPanel addRenameNodePanel = new JPanel(new BorderLayout());
+		addRenameNodePanel.add(this.buildAddButton(), BorderLayout.WEST);
+		addRenameNodePanel.add(this.nameTextField, BorderLayout.CENTER);
+		addRenameNodePanel.add(this.buildRenameButton(), BorderLayout.EAST);
+		return addRenameNodePanel;
+	}
+
+	private Component buildMiscPanel() {
+		JPanel miscPanel = new JPanel(new GridLayout(1, 0));
+		miscPanel.add(this.buildClearChildrenButton());
+		miscPanel.add(this.buildRemoveButton());
+		miscPanel.add(this.buildResetButton());
+		return miscPanel;
+	}
+
+	private String getName() {
+		return this.nameTextField.getText();
+	}
+
+	// ********** queries **********
+	private TestNode selectedNode() {
+		if (this.treeSelectionModel.isSelectionEmpty()) {
+			return null;
+		}
+		return (TestNode) this.treeSelectionModel.getSelectionPath().getLastPathComponent();
+	}
+
+	private TestModel selectedTestModel() {
+		if (this.treeSelectionModel.isSelectionEmpty()) {
+			return null;
+		}
+		return (TestModel) this.selectedNode().getValue();
+	}
+
+	private TestNode rootNode() {
+		return (TestNode) this.treeModel.getRoot();
+	}
+
+	private TestModel root() {
+		return (TestModel) this.rootNode().getValue();
+	}
+
+	private Collection expandedPaths() {
+		Enumeration stream = this.tree.getExpandedDescendants(new TreePath(this.rootNode()));
+		if (stream == null) {
+			return Collections.EMPTY_LIST;
+		}
+		return CollectionTools.list(new EnumerationIterator(stream));
+	}
+
+	// ********** behavior **********
+	private void setSelectedNode(TestNode selectedNode) {
+		this.treeSelectionModel.setSelectionPath(new TreePath(selectedNode.path()));
+	}
+
+	private void expandPaths(Collection paths) {
+		for (Iterator stream = paths.iterator(); stream.hasNext(); ) {
+			this.tree.expandPath((TreePath) stream.next());
+		}
+	}
+
+	// ********** add **********
+	private JButton buildAddButton() {
+		return new JButton(this.buildAddAction());
+	}
+
+	private Action buildAddAction() {
+		Action action = new AbstractAction("add") {
+			public void actionPerformed(ActionEvent event) {
+				TreeModelAdapterUITest.this.addNode();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	/**
+	 * adding causes the tree to be sorted and nodes to be
+	 * removed and re-added; so we have to fiddle with the expansion state
+	 */
+	void addNode() {
+		TestModel selectedTestModel = this.selectedTestModel();
+		if (selectedTestModel != null) {
+			String name = this.getName();
+			// save the expansion state and restore it after the add
+			Collection paths = this.expandedPaths();
+
+			selectedTestModel.addChild(name);
+
+			this.expandPaths(paths);
+			this.setSelectedNode(this.selectedNode().childNamed(name));
+		}
+	}
+
+	// ********** remove **********
+	private JButton buildRemoveButton() {
+		return new JButton(this.buildRemoveAction());
+	}
+
+	private Action buildRemoveAction() {
+		Action action = new AbstractAction("remove") {
+			public void actionPerformed(ActionEvent event) {
+				TreeModelAdapterUITest.this.removeNode();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	/**
+	 * we need to figure out which node to select after
+	 * the selected node is deleted
+	 */
+	void removeNode() {
+		TestModel selectedTestModel = this.selectedTestModel();
+		// do not allow the root to be removed
+		if ((selectedTestModel != null) && (selectedTestModel != this.root())) {
+			// save the parent and index, so we can select another, nearby, node
+			// once the selected node is removed
+			TestNode parentNode = (TestNode) this.selectedNode().getParent();
+			int childIndex = parentNode.indexOfChild(this.selectedNode());
+
+			selectedTestModel.getParent().removeChild(selectedTestModel);
+
+			int childrenSize = parentNode.childrenSize();
+			if (childIndex < childrenSize) {
+				// select the child that moved up and replaced the just-deleted child
+				this.setSelectedNode((TestNode) parentNode.getChild(childIndex));
+			} else {
+				if (childrenSize == 0) {
+					// if there are no more children, select the parent
+					this.setSelectedNode(parentNode);
+				} else {
+					// if the child at the bottom of the list was deleted, select the next child up
+					this.setSelectedNode((TestNode) parentNode.getChild(childIndex - 1));
+				}
+			}
+		}
+	}
+
+	// ********** rename **********
+	private JButton buildRenameButton() {
+		return new JButton(this.buildRenameAction());
+	}
+
+	private Action buildRenameAction() {
+		Action action = new AbstractAction("rename") {
+			public void actionPerformed(ActionEvent event) {
+				TreeModelAdapterUITest.this.renameNode();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	/**
+	 * renaming causes the tree to be sorted and nodes to be
+	 * removed and re-added; so we have to fiddle with the expansion state
+	 */
+	void renameNode() {
+		TestModel selectedTestModel = this.selectedTestModel();
+		if (selectedTestModel != null) {
+			// save the node and re-select it after the rename
+			TestNode selectedNode = this.selectedNode();
+			// save the expansion state and restore it after the rename
+			Collection paths = this.expandedPaths();
+
+			selectedTestModel.setName(this.getName());
+
+			this.expandPaths(paths);
+			this.setSelectedNode(selectedNode);
+		}
+	}
+
+	// ********** clear children **********
+	private JButton buildClearChildrenButton() {
+		return new JButton(this.buildClearChildrenAction());
+	}
+
+	private Action buildClearChildrenAction() {
+		Action action = new AbstractAction("clear children") {
+			public void actionPerformed(ActionEvent event) {
+				TreeModelAdapterUITest.this.clearChildren();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	/**
+	 * nothing special, we just want to test #fireCollectionChanged(String)
+	 */
+	void clearChildren() {
+		TestModel selectedTestModel = this.selectedTestModel();
+		if (selectedTestModel != null) {
+			selectedTestModel.clearChildren();
+		}
+	}
+
+	// ********** reset **********
+	private JButton buildResetButton() {
+		return new JButton(this.buildResetAction());
+	}
+
+	private Action buildResetAction() {
+		Action action = new AbstractAction("reset") {
+			public void actionPerformed(ActionEvent event) {
+				TreeModelAdapterUITest.this.reset();
+			}
+		};
+		action.setEnabled(true);
+		return action;
+	}
+
+	/**
+	 * test the adapter's root node holder;
+	 * toggle between sorted and unsorted lists
+	 */
+	void reset() {
+		this.sorted = ! this.sorted;
+		if (this.sorted) {
+			this.rootNodeHolder.setValue(this.buildSortedRootNode());
+		} else {
+			this.rootNodeHolder.setValue(this.buildUnsortedRootNode());
+		}
+		this.tree.expandPath(new TreePath(this.rootNode()));
+	}
+
+}